feature_map 1.2.5 → 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.
@@ -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
@@ -13,19 +13,9 @@ module FeatureMap
13
13
 
14
14
  FEATURES_KEY = 'features'
15
15
 
16
- def self.write!(unit_examples, integration_examples, regression_examples, regression_assignments)
16
+ def self.write!(unit_by_feature, integration_by_feature, regression_by_feature)
17
17
  FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
18
-
19
- regression_file_assignments = regression_assignments['features']&.transform_values do |feature|
20
- feature['files']&.map { |file| filepath(file) } || []
21
- end || {}
22
-
23
- content = generate_content(
24
- unit_examples.group_by { |ex| filepath(ex['id']) },
25
- integration_examples.group_by { |ex| filepath(ex['id']) },
26
- regression_examples.group_by { |ex| filepath(ex['id']) },
27
- regression_file_assignments
28
- )
18
+ content = generate_content(unit_by_feature, integration_by_feature, regression_by_feature)
29
19
  path.write([header_comment, "\n", { FEATURES_KEY => content }.to_yaml].join)
30
20
  end
31
21
 
@@ -47,36 +37,15 @@ module FeatureMap
47
37
  HEADER
48
38
  end
49
39
 
50
- def self.generate_content(unit_examples, integration_examples, regression_examples, regression_file_assignments)
51
- Private.feature_file_assignments.reduce({}) do |content, (feature_name, files)|
52
- regression_files = regression_file_assignments[feature_name] || []
53
- regression_count, regression_pending = regression_files.reduce([0, 0]) do |accumulated_counts, file|
54
- accumulated_count, accumulated_pending = accumulated_counts
55
- count, pending = split(regression_examples[file])
56
-
57
- [accumulated_count + count, accumulated_pending + pending]
58
- end
59
-
60
- pyramid = files.reduce({}) do |acc, file|
61
- normalized_path = filepath(file)
62
-
63
- unit_count, unit_pending = split(unit_examples["#{normalized_path}_spec"])
64
- integration_count, integration_pending = split(integration_examples[normalized_path])
65
-
66
- {
67
- 'unit_count' => (acc['unit_count'] || 0) + unit_count,
68
- 'unit_pending' => (acc['unit_pending'] || 0) + unit_pending,
69
- 'integration_count' => (acc['integration_count'] || 0) + integration_count,
70
- 'integration_pending' => (acc['integration_pending'] || 0) + integration_pending
71
- }
72
- end
73
-
74
- {
75
- feature_name => pyramid.merge(
76
- 'regression_count' => regression_count,
77
- 'regression_pending' => regression_pending
78
- ),
79
- **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
80
49
  }
81
50
  end
82
51
  end
@@ -92,20 +61,6 @@ module FeatureMap
92
61
  rescue Errno::ENOENT
93
62
  raise FileContentError, "No feature test coverage file found at #{path}. Use `bin/featuremap test_coverage` to generate it and try again."
94
63
  end
95
-
96
- def self.split(examples)
97
- return [0, 0] if examples.nil?
98
-
99
- examples.partition { |ex| ex['status'] == 'passed' }.map(&:count)
100
- end
101
-
102
- def self.filepath(pathlike)
103
- File
104
- .join(File.dirname(pathlike), File.basename(pathlike, '.*'))
105
- .gsub(%r{^\./}, '')
106
- .gsub(%r{^spec/}, '')
107
- .gsub(%r{^app/}, '')
108
- end
109
64
  end
110
65
  end
111
66
  end
@@ -24,6 +24,9 @@ require 'feature_map/private/test_pyramid_file'
24
24
  require 'feature_map/private/additional_metrics_file'
25
25
  require 'feature_map/private/simple_cov_resultsets'
26
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'
27
30
  require 'feature_map/private/validations/files_have_features'
28
31
  require 'feature_map/private/validations/features_up_to_date'
29
32
  require 'feature_map/private/validations/files_have_unique_features'
@@ -105,11 +108,14 @@ module FeatureMap
105
108
  end
106
109
 
107
110
  def self.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
108
- unit_examples = JSON.parse(File.read(unit_path))&.fetch('examples')
109
- integration_examples = JSON.parse(File.read(integration_path))&.fetch('examples')
110
- regression_examples = regression_path ? JSON.parse(File.read(regression_path))&.fetch('examples') : []
111
- regression_assignments = regression_assignments_path ? YAML.load_file(regression_assignments_path) : {}
112
- 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
+ )
113
119
  end
114
120
 
115
121
  def self.gather_simplecov_test_coverage!(simplecov_paths)
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.5
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-13 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
@@ -189,9 +189,13 @@ extensions: []
189
189
  extra_rdoc_files: []
190
190
  files:
191
191
  - README.md
192
+ - bin/bundle
192
193
  - bin/docs
193
194
  - bin/featuremap
195
+ - bin/jekyll
194
196
  - bin/readme
197
+ - bin/rspec
198
+ - bin/rubocop
195
199
  - lib/feature_map.rb
196
200
  - lib/feature_map/cli.rb
197
201
  - lib/feature_map/code_features.rb
@@ -226,6 +230,9 @@ files:
226
230
  - lib/feature_map/private/release_notification_builder.rb
227
231
  - lib/feature_map/private/simple_cov_resultsets.rb
228
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
229
236
  - lib/feature_map/private/test_pyramid_file.rb
230
237
  - lib/feature_map/private/todo_inspector.rb
231
238
  - lib/feature_map/private/validations/features_up_to_date.rb