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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/bin/bundle +115 -0
- data/bin/docs +4 -4
- 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/private/docs/index.html +52 -52
- 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 +11 -56
- data/lib/feature_map/private.rb +11 -5
- metadata +9 -2
@@ -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!(
|
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(
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
data/lib/feature_map/private.rb
CHANGED
@@ -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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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.
|
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
|
@@ -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
|