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.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/bin/bundle +115 -0
- data/bin/docs +19 -1
- data/bin/featuremap +3 -0
- data/bin/jekyll +27 -0
- data/bin/readme +1 -0
- data/bin/rspec +27 -0
- data/bin/rubocop +27 -0
- data/lib/feature_map/cli.rb +45 -7
- data/lib/feature_map/code_features.rb +1 -0
- data/lib/feature_map/commit.rb +1 -0
- data/lib/feature_map/configuration.rb +1 -0
- data/lib/feature_map/constants.rb +1 -0
- data/lib/feature_map/mapper.rb +1 -0
- data/lib/feature_map/output_color.rb +1 -0
- data/lib/feature_map/private/additional_metrics_file.rb +1 -0
- data/lib/feature_map/private/assignment_applicator.rb +1 -0
- data/lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb +14 -0
- data/lib/feature_map/private/assignments_file.rb +1 -0
- data/lib/feature_map/private/code_cov.rb +1 -0
- data/lib/feature_map/private/cyclomatic_complexity_calculator.rb +1 -0
- data/lib/feature_map/private/docs/index.html +99 -94
- data/lib/feature_map/private/documentation_site.rb +1 -0
- data/lib/feature_map/private/extension_loader.rb +1 -0
- data/lib/feature_map/private/feature_assigner.rb +1 -0
- data/lib/feature_map/private/feature_metrics_calculator.rb +1 -0
- data/lib/feature_map/private/feature_plugins/assignment.rb +1 -0
- data/lib/feature_map/private/glob_cache.rb +1 -0
- data/lib/feature_map/private/health_calculator.rb +1 -0
- data/lib/feature_map/private/lines_of_code_calculator.rb +1 -0
- data/lib/feature_map/private/metrics_file.rb +1 -0
- data/lib/feature_map/private/percentile_metrics_calculator.rb +1 -0
- data/lib/feature_map/private/release_notification_builder.rb +1 -0
- data/lib/feature_map/private/simple_cov_resultsets.rb +30 -0
- data/lib/feature_map/private/test_coverage_file.rb +1 -0
- data/lib/feature_map/private/test_pyramid/jest_mapper.rb +44 -0
- data/lib/feature_map/private/test_pyramid/mapper.rb +33 -0
- data/lib/feature_map/private/test_pyramid/rspec_mapper.rb +54 -0
- data/lib/feature_map/private/test_pyramid_file.rb +12 -56
- data/lib/feature_map/private/todo_inspector.rb +1 -0
- data/lib/feature_map/private.rb +20 -5
- data/lib/feature_map/validator.rb +1 -0
- data/lib/feature_map.rb +5 -0
- metadata +24 -2
@@ -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
|
@@ -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!(
|
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(
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
data/lib/feature_map/private.rb
CHANGED
@@ -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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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)
|
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
|
+
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-
|
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
|