feature_map 1.2.4 → 1.2.6

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/bin/bundle +115 -0
  4. data/bin/docs +19 -1
  5. data/bin/featuremap +3 -0
  6. data/bin/jekyll +27 -0
  7. data/bin/readme +1 -0
  8. data/bin/rspec +27 -0
  9. data/bin/rubocop +27 -0
  10. data/lib/feature_map/cli.rb +45 -7
  11. data/lib/feature_map/code_features.rb +1 -0
  12. data/lib/feature_map/commit.rb +1 -0
  13. data/lib/feature_map/configuration.rb +1 -0
  14. data/lib/feature_map/constants.rb +1 -0
  15. data/lib/feature_map/mapper.rb +1 -0
  16. data/lib/feature_map/output_color.rb +1 -0
  17. data/lib/feature_map/private/additional_metrics_file.rb +1 -0
  18. data/lib/feature_map/private/assignment_applicator.rb +1 -0
  19. data/lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb +14 -0
  20. data/lib/feature_map/private/assignments_file.rb +1 -0
  21. data/lib/feature_map/private/code_cov.rb +1 -0
  22. data/lib/feature_map/private/cyclomatic_complexity_calculator.rb +1 -0
  23. data/lib/feature_map/private/docs/index.html +99 -94
  24. data/lib/feature_map/private/documentation_site.rb +1 -0
  25. data/lib/feature_map/private/extension_loader.rb +1 -0
  26. data/lib/feature_map/private/feature_assigner.rb +1 -0
  27. data/lib/feature_map/private/feature_metrics_calculator.rb +1 -0
  28. data/lib/feature_map/private/feature_plugins/assignment.rb +1 -0
  29. data/lib/feature_map/private/glob_cache.rb +1 -0
  30. data/lib/feature_map/private/health_calculator.rb +1 -0
  31. data/lib/feature_map/private/lines_of_code_calculator.rb +1 -0
  32. data/lib/feature_map/private/metrics_file.rb +1 -0
  33. data/lib/feature_map/private/percentile_metrics_calculator.rb +1 -0
  34. data/lib/feature_map/private/release_notification_builder.rb +1 -0
  35. data/lib/feature_map/private/simple_cov_resultsets.rb +30 -0
  36. data/lib/feature_map/private/test_coverage_file.rb +1 -0
  37. data/lib/feature_map/private/test_pyramid/jest_mapper.rb +44 -0
  38. data/lib/feature_map/private/test_pyramid/mapper.rb +33 -0
  39. data/lib/feature_map/private/test_pyramid/rspec_mapper.rb +54 -0
  40. data/lib/feature_map/private/test_pyramid_file.rb +12 -56
  41. data/lib/feature_map/private/todo_inspector.rb +1 -0
  42. data/lib/feature_map/private.rb +20 -5
  43. data/lib/feature_map/validator.rb +1 -0
  44. data/lib/feature_map.rb +5 -0
  45. metadata +24 -2
@@ -1,3 +1,4 @@
1
+ # @feature Extension System
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Extension System
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Feature Assignment
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Metrics Calculation
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'rubocop'
@@ -1,3 +1,4 @@
1
+ # @feature Code Features
1
2
  module FeatureMap
2
3
  module Private
3
4
  module FeaturePlugins
@@ -1,3 +1,4 @@
1
+ # @feature Assignment Mapping
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Metrics Calculation
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Metrics Calculation
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'parser/current'
@@ -1,3 +1,4 @@
1
+ # @feature Metrics Storage
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Metrics Calculation
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Extension System
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -0,0 +1,30 @@
1
+ # @feature Testing Tools
2
+ class SimpleCovResultsets
3
+ def self.fetch_coverage_stats(simplecov_resultsets)
4
+ root_dir = "#{Dir.pwd}/"
5
+
6
+ simplecov_resultsets.reduce({}) do |combined_stats, simplecov_resultset|
7
+ coverage_data = simplecov_resultset['RSpec']['coverage']
8
+
9
+ file_stats = coverage_data.map do |absolute_path, file_data|
10
+ relative_path = absolute_path.sub(root_dir, '')
11
+ lines_data = file_data['lines']
12
+
13
+ executable_lines = lines_data.compact.size
14
+ covered_lines = lines_data.count { |line| line&.positive? }
15
+ missed_lines = executable_lines - covered_lines
16
+ coverage_percentage = executable_lines.positive? ? ((covered_lines.to_f / executable_lines) * 100).round(2) : 0
17
+
18
+ # Return a tuple of [path, stats]
19
+ [relative_path, {
20
+ 'lines' => executable_lines,
21
+ 'hits' => covered_lines,
22
+ 'misses' => missed_lines,
23
+ 'coverage_ratio' => coverage_percentage
24
+ }]
25
+ end
26
+
27
+ combined_stats.merge(file_stats.to_h)
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,4 @@
1
+ # @feature Testing Tools
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -0,0 +1,44 @@
1
+ module FeatureMap
2
+ module Private
3
+ module TestPyramid
4
+ class JestMapper
5
+ class << self
6
+ def map_tests_by_assignment(test_suites, assignments)
7
+ # Transform test suites into a hash of filepath => assertion results
8
+ tests_by_path = test_suites.each_with_object({}) do |suite, result|
9
+ result[filepath(suite['name'])] = suite['assertionResults']
10
+ end
11
+
12
+ assignments.each_with_object({}) do |(feature_name, files), result|
13
+ counts = count_tests_for_feature(tests_by_path, files)
14
+ result[feature_name] = counts
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def count_tests_for_feature(tests_by_path, files)
21
+ files.each_with_object({ count: 0, pending: 0 }) do |file, counts|
22
+ file_path = filepath(file)
23
+ assertions = tests_by_path[file_path]
24
+ next unless assertions
25
+
26
+ passed, pending = assertions.partition { |assertion| !%w[pending skipped todo].include?(assertion['status']) }
27
+ counts[:count] += passed.size
28
+ counts[:pending] += pending.size
29
+ end
30
+ end
31
+
32
+ def filepath(pathlike)
33
+ # Get the base directory of the script execution
34
+ project_root = Dir.pwd
35
+ path = File.join(File.dirname(pathlike), File.basename(pathlike, '.*'))
36
+
37
+ # Strip absolute path prefix, keeping path relative to the project root
38
+ path.gsub("#{project_root}/", '')
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ module FeatureMap
2
+ module Private
3
+ module TestPyramid
4
+ class Mapper
5
+ class << self
6
+ def examples_by_feature(examples_path, assignments)
7
+ return {} if examples_path =~ /.skip$/
8
+
9
+ examples_file = File.read(examples_path)
10
+ normalized_assignments = assignments.transform_values { |feature| feature['files'] || [] }
11
+
12
+ case examples_path
13
+ when /\.rspec$/
14
+ examples = JSON.parse(examples_file)&.fetch('examples', [])
15
+ TestPyramid::RspecMapper.map_tests_by_assignment(
16
+ examples,
17
+ normalized_assignments
18
+ )
19
+ when /\.jest$/
20
+ examples = JSON.parse(examples_file)&.fetch('testResults', [])
21
+ TestPyramid::JestMapper.map_tests_by_assignment(
22
+ examples,
23
+ normalized_assignments
24
+ )
25
+ else
26
+ raise "Unhandled filetype for unit path: #{examples_path}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,54 @@
1
+ module FeatureMap
2
+ module Private
3
+ module TestPyramid
4
+ class RspecMapper
5
+ class << self
6
+ def map_tests_by_assignment(examples, assignments)
7
+ normalized_assignments = transform_assignments(assignments)
8
+ examples_by_file = examples.group_by { |ex| filepath(ex['id']) }
9
+
10
+ normalized_assignments.transform_values do |files|
11
+ count_example_files(examples_by_file, files)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def count_example_files(examples_by_file, files)
18
+ files.each_with_object({ count: 0, pending: 0 }) do |file, counts|
19
+ file_examples = examples_by_file[file]
20
+ next unless file_examples
21
+
22
+ passed, pending = file_examples.partition { |ex| ex['status'] == 'passed' }
23
+ counts[:count] += passed.size
24
+ counts[:pending] += pending.size
25
+ end
26
+ end
27
+
28
+ # NOTE: We normalize paths to remove the app/ and spec/ prefix,
29
+ # as well as the _spec suffix. This allows for spec files to
30
+ # be mapped to feature-assigned app-paths such that the specs
31
+ # do not need to be directly assigned to features. It still
32
+ # works if they are directly assigned to features, too.
33
+ def filepath(pathlike)
34
+ File
35
+ .join(File.dirname(pathlike), File.basename(pathlike, '.*'))
36
+ .gsub(%r{^\./}, '')
37
+ .gsub(%r{^spec/}, '')
38
+ .gsub(%r{^app/}, '')
39
+ .gsub(/_spec$/, '')
40
+ end
41
+
42
+ def transform_assignments(assignments)
43
+ assignments.transform_values do |files|
44
+ [
45
+ *files,
46
+ files.map { |f| filepath(f) }
47
+ ].flatten
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,4 @@
1
+ # @feature Testing Tools
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -12,19 +13,9 @@ module FeatureMap
12
13
 
13
14
  FEATURES_KEY = 'features'
14
15
 
15
- def self.write!(unit_examples, integration_examples, regression_examples, regression_assignments)
16
+ def self.write!(unit_by_feature, integration_by_feature, regression_by_feature)
16
17
  FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
17
-
18
- regression_file_assignments = regression_assignments['features']&.transform_values do |feature|
19
- feature['files']&.map { |file| filepath(file) } || []
20
- end || {}
21
-
22
- content = generate_content(
23
- unit_examples.group_by { |ex| filepath(ex['id']) },
24
- integration_examples.group_by { |ex| filepath(ex['id']) },
25
- regression_examples.group_by { |ex| filepath(ex['id']) },
26
- regression_file_assignments
27
- )
18
+ content = generate_content(unit_by_feature, integration_by_feature, regression_by_feature)
28
19
  path.write([header_comment, "\n", { FEATURES_KEY => content }.to_yaml].join)
29
20
  end
30
21
 
@@ -46,36 +37,15 @@ module FeatureMap
46
37
  HEADER
47
38
  end
48
39
 
49
- def self.generate_content(unit_examples, integration_examples, regression_examples, regression_file_assignments)
50
- Private.feature_file_assignments.reduce({}) do |content, (feature_name, files)|
51
- regression_files = regression_file_assignments[feature_name] || []
52
- regression_count, regression_pending = regression_files.reduce([0, 0]) do |accumulated_counts, file|
53
- accumulated_count, accumulated_pending = accumulated_counts
54
- count, pending = split(regression_examples[file])
55
-
56
- [accumulated_count + count, accumulated_pending + pending]
57
- end
58
-
59
- pyramid = files.reduce({}) do |acc, file|
60
- normalized_path = filepath(file)
61
-
62
- unit_count, unit_pending = split(unit_examples["#{normalized_path}_spec"])
63
- integration_count, integration_pending = split(integration_examples[normalized_path])
64
-
65
- {
66
- 'unit_count' => (acc['unit_count'] || 0) + unit_count,
67
- 'unit_pending' => (acc['unit_pending'] || 0) + unit_pending,
68
- 'integration_count' => (acc['integration_count'] || 0) + integration_count,
69
- 'integration_pending' => (acc['integration_pending'] || 0) + integration_pending
70
- }
71
- end
72
-
73
- {
74
- feature_name => pyramid.merge(
75
- 'regression_count' => regression_count,
76
- 'regression_pending' => regression_pending
77
- ),
78
- **content
40
+ def self.generate_content(unit_by_feature, integration_by_feature, regression_by_feature)
41
+ CodeFeatures.all.map(&:name).each_with_object({}) do |feature_name, content|
42
+ content[feature_name] = {
43
+ 'unit_count' => unit_by_feature.dig(feature_name, :count) || 0,
44
+ 'unit_pending' => unit_by_feature.dig(feature_name, :pending) || 0,
45
+ 'integration_count' => integration_by_feature.dig(feature_name, :count) || 0,
46
+ 'integration_pending' => integration_by_feature.dig(feature_name, :pending) || 0,
47
+ 'regression_count' => regression_by_feature.dig(feature_name, :count) || 0,
48
+ 'regression_pending' => regression_by_feature.dig(feature_name, :pending) || 0
79
49
  }
80
50
  end
81
51
  end
@@ -91,20 +61,6 @@ module FeatureMap
91
61
  rescue Errno::ENOENT
92
62
  raise FileContentError, "No feature test coverage file found at #{path}. Use `bin/featuremap test_coverage` to generate it and try again."
93
63
  end
94
-
95
- def self.split(examples)
96
- return [0, 0] if examples.nil?
97
-
98
- examples.partition { |ex| ex['status'] == 'passed' }.map(&:count)
99
- end
100
-
101
- def self.filepath(pathlike)
102
- File
103
- .join(File.dirname(pathlike), File.basename(pathlike, '.*'))
104
- .gsub(%r{^\./}, '')
105
- .gsub(%r{^spec/}, '')
106
- .gsub(%r{^app/}, '')
107
- end
108
64
  end
109
65
  end
110
66
  end
@@ -1,3 +1,4 @@
1
+ # @feature Testing Tools
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module FeatureMap
@@ -1,3 +1,4 @@
1
+ # @feature Core Library
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'code_ownership'
@@ -21,7 +22,11 @@ require 'feature_map/private/code_cov'
21
22
  require 'feature_map/private/test_coverage_file'
22
23
  require 'feature_map/private/test_pyramid_file'
23
24
  require 'feature_map/private/additional_metrics_file'
25
+ require 'feature_map/private/simple_cov_resultsets'
24
26
  require 'feature_map/private/feature_plugins/assignment'
27
+ require 'feature_map/private/test_pyramid/mapper'
28
+ require 'feature_map/private/test_pyramid/rspec_mapper'
29
+ require 'feature_map/private/test_pyramid/jest_mapper'
25
30
  require 'feature_map/private/validations/files_have_features'
26
31
  require 'feature_map/private/validations/features_up_to_date'
27
32
  require 'feature_map/private/validations/files_have_unique_features'
@@ -103,11 +108,21 @@ module FeatureMap
103
108
  end
104
109
 
105
110
  def self.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
106
- unit_examples = JSON.parse(File.read(unit_path))&.fetch('examples')
107
- integration_examples = JSON.parse(File.read(integration_path))&.fetch('examples')
108
- regression_examples = regression_path ? JSON.parse(File.read(regression_path))&.fetch('examples') : []
109
- regression_assignments = regression_assignments_path ? YAML.load_file(regression_assignments_path) : {}
110
- TestPyramidFile.write!(unit_examples, integration_examples, regression_examples, regression_assignments)
111
+ assignments = AssignmentsFile.load_features!
112
+ regression_assignments = regression_assignments_path ? YAML.load_file(regression_assignments_path)&.fetch('features') : assignments
113
+
114
+ TestPyramidFile.write!(
115
+ TestPyramid::Mapper.examples_by_feature(unit_path, assignments),
116
+ TestPyramid::Mapper.examples_by_feature(integration_path, assignments),
117
+ regression_path ? TestPyramid::Mapper.examples_by_feature(regression_path, regression_assignments) : {}
118
+ )
119
+ end
120
+
121
+ def self.gather_simplecov_test_coverage!(simplecov_paths)
122
+ simplecov_resultsets = simplecov_paths.map { |path| JSON.parse(File.read(path)) }
123
+ coverage_stats = SimpleCovResultsets.fetch_coverage_stats(simplecov_resultsets)
124
+
125
+ TestCoverageFile.write!(coverage_stats)
111
126
  end
112
127
 
113
128
  def self.gather_test_coverage!(commit_sha, code_cov_token)
@@ -1,3 +1,4 @@
1
+ # @feature Core Library
1
2
  module FeatureMap
2
3
  module Validator
3
4
  def validation_errors(files:, autocorrect: true, stage_changes: true); end
data/lib/feature_map.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # @feature Core Library
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'set'
@@ -97,6 +98,10 @@ module FeatureMap
97
98
  Private.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
98
99
  end
99
100
 
101
+ def gather_simplecov_test_coverage!(simplecov_resultsets)
102
+ Private.gather_simplecov_test_coverage!(simplecov_resultsets)
103
+ end
104
+
100
105
  def gather_test_coverage!(commit_sha, code_cov_token)
101
106
  Private.gather_test_coverage!(commit_sha, code_cov_token)
102
107
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feature_map
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.4
4
+ version: 1.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Beyond Finance
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-07 00:00:00.000000000 Z
11
+ date: 2025-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: code_ownership
@@ -150,6 +150,20 @@ dependencies:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
152
  version: '3.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  - !ruby/object:Gem::Dependency
154
168
  name: webmock
155
169
  requirement: !ruby/object:Gem::Requirement
@@ -175,9 +189,13 @@ extensions: []
175
189
  extra_rdoc_files: []
176
190
  files:
177
191
  - README.md
192
+ - bin/bundle
178
193
  - bin/docs
179
194
  - bin/featuremap
195
+ - bin/jekyll
180
196
  - bin/readme
197
+ - bin/rspec
198
+ - bin/rubocop
181
199
  - lib/feature_map.rb
182
200
  - lib/feature_map/cli.rb
183
201
  - lib/feature_map/code_features.rb
@@ -210,7 +228,11 @@ files:
210
228
  - lib/feature_map/private/metrics_file.rb
211
229
  - lib/feature_map/private/percentile_metrics_calculator.rb
212
230
  - lib/feature_map/private/release_notification_builder.rb
231
+ - lib/feature_map/private/simple_cov_resultsets.rb
213
232
  - lib/feature_map/private/test_coverage_file.rb
233
+ - lib/feature_map/private/test_pyramid/jest_mapper.rb
234
+ - lib/feature_map/private/test_pyramid/mapper.rb
235
+ - lib/feature_map/private/test_pyramid/rspec_mapper.rb
214
236
  - lib/feature_map/private/test_pyramid_file.rb
215
237
  - lib/feature_map/private/todo_inspector.rb
216
238
  - lib/feature_map/private/validations/features_up_to_date.rb