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



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

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.



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

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

.configurationObject



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

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.



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

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



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

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

.for_class(klass) ⇒ Object



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

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



42
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
# File 'lib/feature_map.rb', line 42

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



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

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



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

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

.gather_test_coverage!(commit_sha, code_cov_token) ⇒ Object



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

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

.generate_additional_metrics!Object



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

def generate_additional_metrics!
  Private.generate_additional_metrics!
end

.generate_docs!(git_ref) ⇒ Object



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

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.



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

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



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

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.



181
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
# File 'lib/feature_map.rb', line 181

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



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

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

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



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

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