Module: FeatureMap

Defined in:
lib/feature_map/commit.rb,
lib/feature_map.rb,
lib/feature_map/cli.rb,
lib/feature_map/mapper.rb,
lib/feature_map/private.rb,
lib/feature_map/constants.rb,
lib/feature_map/validator.rb,
lib/feature_map/code_features.rb,
lib/feature_map/configuration.rb,
lib/feature_map/private/code_cov.rb,
lib/feature_map/private/glob_cache.rb,
lib/feature_map/code_features/plugin.rb,
lib/feature_map/private/metrics_file.rb,
lib/feature_map/private/todo_inspector.rb,
lib/feature_map/private/assignments_file.rb,
lib/feature_map/private/extension_loader.rb,
lib/feature_map/private/feature_assigner.rb,
lib/feature_map/private/health_calculator.rb,
lib/feature_map/private/test_pyramid_file.rb,
lib/feature_map/private/documentation_site.rb,
lib/feature_map/private/test_coverage_file.rb,
lib/feature_map/private/test_pyramid/mapper.rb,
lib/feature_map/private/assignment_applicator.rb,
lib/feature_map/code_features/plugins/identity.rb,
lib/feature_map/private/additional_metrics_file.rb,
lib/feature_map/private/lines_of_code_calculator.rb,
lib/feature_map/private/test_pyramid/jest_mapper.rb,
lib/feature_map/private/test_pyramid/rspec_mapper.rb,
lib/feature_map/private/feature_metrics_calculator.rb,
lib/feature_map/private/feature_plugins/assignment.rb,
lib/feature_map/private/release_notification_builder.rb,
lib/feature_map/private/percentile_metrics_calculator.rb,
lib/feature_map/private/validations/features_up_to_date.rb,
lib/feature_map/private/validations/files_have_features.rb,
lib/feature_map/private/assignment_mappers/feature_globs.rb,
lib/feature_map/private/cyclomatic_complexity_calculator.rb,
lib/feature_map/private/assignment_mappers/file_annotations.rb,
lib/feature_map/private/validations/files_have_unique_features.rb,
lib/feature_map/private/assignment_mappers/directory_assignment.rb,
lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb

Overview

frozen_string_literal: true

Defined Under Namespace

Modules: CodeFeatures, Constants, Mapper, Validator Classes: Cli, Commit, Configuration, InvalidFeatureMapConfigurationError

Constant Summary collapse

ALL_TEAMS_KEY =
'All Teams'
NO_FEATURE_KEY =
'No Feature'

Class Method Summary collapse

Class Method Details

.apply_assignments!(assignments_file_path) ⇒ Object



22
23
24
# File 'lib/feature_map.rb', line 22

def apply_assignments!(assignments_file_path)
  Private.apply_assignments!(assignments_file_path)
end

.bust_caches!Object

Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change. Namely, the set of files, and directories which are tracked for feature assignment should not change. The primary reason this is helpful is for clients of FeatureMap who want to test their code, and each test context has different feature assignments and tracked files.



222
223
224
225
226
227
# File 'lib/feature_map.rb', line 222

def self.bust_caches!
  @for_file = nil
  @memoized_values = nil
  Private.bust_caches!
  Mapper.all.each(&:bust_caches!)
end

.configurationObject



229
230
231
# File 'lib/feature_map.rb', line 229

def self.configuration
  Private.configuration
end

.first_assigned_file_for_backtrace(backtrace, excluded_features: []) ⇒ Object

Given a backtrace from either ‘Exception#backtrace` or `caller`, find the first assigned file in it, useful for figuring out which file is being blamed.



121
122
123
124
125
126
127
128
129
# File 'lib/feature_map.rb', line 121

def first_assigned_file_for_backtrace(backtrace, excluded_features: [])
  backtrace_with_feature_assignments(backtrace).each do |(feature, file)|
    if feature && !excluded_features.include?(feature)
      return [feature, file]
    end
  end

  nil
end

.for_backtrace(backtrace, excluded_features: []) ⇒ Object

Given a backtrace from either ‘Exception#backtrace` or `caller`, find the first line that corresponds to a file with an assigned feature



115
116
117
# File 'lib/feature_map.rb', line 115

def for_backtrace(backtrace, excluded_features: [])
  first_assigned_file_for_backtrace(backtrace, excluded_features: excluded_features)&.first
end

.for_class(klass) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/feature_map.rb', line 163

def for_class(klass)
  @memoized_values ||= {}
  # We use key because the memoized value could be `nil`
  if @memoized_values.key?(klass.to_s)
    @memoized_values[klass.to_s]
  else
    path = Private.path_from_klass(klass)
    return nil if path.nil?

    value_to_memoize = for_file(path)
    @memoized_values[klass.to_s] = value_to_memoize
    value_to_memoize
  end
end

.for_feature(feature) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/feature_map.rb', line 43

def for_feature(feature)
  feature = CodeFeatures.find(feature)
  feature_report = []

  feature_report << "# Report for `#{feature.name}` Feature"

  Private.glob_cache.raw_cache_contents.each do |mapper_description, glob_to_assigned_feature_map|
    feature_report << "## #{mapper_description}"
    file_assignments_for_mapper = []
    glob_to_assigned_feature_map.each do |glob, assigned_feature|
      next if assigned_feature != feature

      file_assignments_for_mapper << "- #{glob}"
    end

    if file_assignments_for_mapper.empty?
      feature_report << 'This feature does not have any files in this category.'
    else
      feature_report += file_assignments_for_mapper.sort
    end

    feature_report << ''
  end

  feature_report.join("\n")
end

.for_file(file) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/feature_map.rb', line 26

def for_file(file)
  @for_file ||= {}

  return nil if file.start_with?('./')
  return @for_file[file] if @for_file.key?(file)

  Private.load_configuration!

  feature = nil
  Mapper.all.each do |mapper|
    feature = mapper.map_file_to_feature(file)
    break if feature # TODO: what if there are multiple features? Should we respond with an error instead of the first match?
  end

  @for_file[file] = feature
end

.gather_simplecov_test_coverage!(simplecov_resultsets) ⇒ Object



101
102
103
# File 'lib/feature_map.rb', line 101

def gather_simplecov_test_coverage!(simplecov_resultsets)
  Private.gather_simplecov_test_coverage!(simplecov_resultsets)
end

.gather_test_coverage!(commit_sha, code_cov_token) ⇒ Object



105
106
107
# File 'lib/feature_map.rb', line 105

def gather_test_coverage!(commit_sha, code_cov_token)
  Private.gather_test_coverage!(commit_sha, code_cov_token)
end

.generate_additional_metrics!Object



109
110
111
# File 'lib/feature_map.rb', line 109

def generate_additional_metrics!
  Private.generate_additional_metrics!
end

.generate_docs!(git_ref) ⇒ Object



93
94
95
# File 'lib/feature_map.rb', line 93

def generate_docs!(git_ref)
  Private.generate_docs!(git_ref)
end

.generate_release_notification(commits_by_feature) ⇒ Object

Generates a block kit message grouping the provided commits into sections for each feature impacted by the cheanges.



213
214
215
# File 'lib/feature_map.rb', line 213

def generate_release_notification(commits_by_feature)
  Private.generate_release_notification(commits_by_feature)
end

.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path) ⇒ Object



97
98
99
# File 'lib/feature_map.rb', line 97

def generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
  Private.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
end

.group_commits(commits) ⇒ Object

Groups the provided list of commits (e.g. the changes being deployed in a release) by both the feature they impact and the teams responsible for these features. Returns a hash with keys for each team with features modified within these commits and values that are a hash of features to the set of commits that impact each feature.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/feature_map.rb', line 182

def group_commits(commits)
  commits.each_with_object({}) do |commit, hash|
    commit_features = commit.files.map do |file|
      feature = FeatureMap.for_file(file)
      next nil unless feature

      teams = Private.all_teams_for_feature(feature)
      team_names = teams.empty? ? [ALL_TEAMS_KEY] : teams.map(&:name)

      team_names.sort.each do |team_name|
        hash[team_name] ||= {}
        hash[team_name][feature.name] ||= []
        hash[team_name][feature.name] << commit unless hash[team_name][feature.name].include?(commit)
      end

      feature
    end

    # If the commit did not have any files that relate to a specific feature, include it in a "No Feature" section
    # of the "All Teams" grouping to avoid it being omitted from the resulting grouped commits entirely.
    next unless commit_features.compact.empty?

    hash[ALL_TEAMS_KEY] ||= {}
    hash[ALL_TEAMS_KEY][NO_FEATURE_KEY] ||= []
    hash[ALL_TEAMS_KEY][NO_FEATURE_KEY] << commit
  end
end

.remove_file_annotation!(filename) ⇒ Object



73
74
75
# File 'lib/feature_map.rb', line 73

def self.remove_file_annotation!(filename)
  Private::AssignmentMappers::FileAnnotations.new.remove_file_annotation!(filename)
end

.validate!(autocorrect: true, stage_changes: true, files: nil) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/feature_map.rb', line 77

def validate!(
  autocorrect: true,
  stage_changes: true,
  files: nil
)
  Private.load_configuration!

  tracked_file_subset = if files
                          files.select { |f| Private.file_tracked?(f) }
                        else
                          Private.tracked_files
                        end

  Private.validate!(files: tracked_file_subset, autocorrect: autocorrect, stage_changes: stage_changes)
end