Class: Refinement::Analyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/refinement/analyzer.rb

Overview

Analyzes changes in a repository and determines how those changes impact the targets in Xcode projects in the workspace.

Instance Method Summary collapse

Constructor Details

#initialize(changesets:, workspace_path:, projects: nil, augmenting_paths_yaml_files:, augmenting_paths_by_target: nil) ⇒ Analyzer

Initializes an analyzer with changesets, projects, and augmenting paths.

Parameters:

  • changesets (Array<Changeset>)
  • workspace_path (Pathname)

    path to a root workspace or project, must be ‘nil` if `projects` are specified explicitly

  • projects (Array<Xcodeproj::Project>) (defaults to: nil)

    projects to find targets in, must not be specified if ‘workspace_path` is not `nil`

  • augmenting_paths_yaml_files (Array<Pathname>)

    paths to YAML files that provide augmenting paths by target, must be ‘nil` if `augmenting_paths_by_target` are specified explicitly

  • augmenting_paths_by_target (Hash<String, Array>) (defaults to: nil)

    arrays of hashes keyed by target name (or ‘*’ for all targets) describing paths or globs that each target should be considered to be using, must not be specified if ‘augmenting_paths_yaml_files` is not `nil`

Raises:

  • (ArgumentError)

    when conflicting arguments are given



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

def initialize(changesets:, workspace_path:, projects: nil,
               augmenting_paths_yaml_files:, augmenting_paths_by_target: nil)

  @changesets = changesets

  raise ArgumentError, 'Can only specify one of workspace_path and projects' if workspace_path && projects

  @workspace_path = workspace_path
  @projects = projects

  raise ArgumentError, 'Can only specify one of augmenting_paths_yaml_files and augmenting_paths_by_target' if augmenting_paths_yaml_files && augmenting_paths_by_target

  @augmenting_paths_yaml_files = augmenting_paths_yaml_files
  @augmenting_paths_by_target = augmenting_paths_by_target
end

Instance Method Details

#annotate_targets!Array<AnnotatedTarget>

Returns targets from the projects annotated with their changes, based upon the changeset.

Returns:

  • (Array<AnnotatedTarget>)

    targets from the projects annotated with their changes, based upon the changeset



43
44
45
# File 'lib/refinement/analyzer.rb', line 43

def annotate_targets!
  @annotate_targets ||= annotated_targets
end

#filtered_scheme(scheme_path:, change_level: :full_transitive, filter_when_scheme_has_changed: false, log_changes: false, filter_scheme_for_build_action:, each_target: nil) ⇒ Xcodeproj::XCScheme

Returns a scheme whose unchanged targets have been removed.

Parameters:

  • scheme_path (Pathname)

    the absolute path to the scheme to be filtered

  • change_level (Symbol) (defaults to: :full_transitive)

    the change level at which a target must have changed in order to remain in the scheme. defaults to ‘:full_transitive`

  • filter_when_scheme_has_changed (Boolean) (defaults to: false)

    whether the scheme should be filtered even when the changeset includes the scheme’s path as changed. Defaults to ‘false`

  • log_changes (Boolean) (defaults to: false)

    whether modifications to the scheme are logged. Defaults to ‘false`

  • filter_scheme_for_build_action (:building, :testing)

    The xcodebuild action the scheme is being filtered for. The currently supported values are ‘:building` and `:testing`, with the only difference being `BuildActionEntry` are not filtered out when building for testing, since test action macro expansion could depend on a build entry being present.

  • each_target (Proc) (defaults to: nil)

    A proc called each time a target was determined to have changed or not.

Returns:

  • (Xcodeproj::XCScheme)

    a scheme whose unchanged targets have been removed.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/refinement/analyzer.rb', line 62

def filtered_scheme(scheme_path:, change_level: :full_transitive, filter_when_scheme_has_changed: false, log_changes: false,
                    filter_scheme_for_build_action:, each_target: nil)
  scheme = Xcodeproj::XCScheme.new(scheme_path)

  sections_to_filter =
    case filter_scheme_for_build_action
    when :building
      %w[BuildActionEntry TestableReference]
    when :testing
      # don't want to filter out build action entries running
      # xcodebuild build-for-testing / test, since the test action could have a macro expansion
      # that depends upon one of the build targets.
      %w[TestableReference]
    else
      raise ArgumentError,
            'The supported values for the `filter_scheme_for_build_action` parameter are: [:building, :testing]. ' \
            "Given: #{filter_scheme_for_build_action.inspect}."
    end

  if !filter_when_scheme_has_changed &&
     UsedPath.new(path: Pathname(scheme_path), inclusion_reason: 'scheme').find_in_changesets(changesets)
    return scheme
  end

  changes_by_suite_name = Hash[annotate_targets!
                          .map { |at| [at.xcode_target.name, at.change_reason(level: change_level)] }]

  doc = scheme.doc

  xpaths = sections_to_filter.map { |section| "//*/#{section}/BuildableReference" }
  xpaths.each do |xpath|
    doc.get_elements(xpath).to_a.each do |buildable_reference|
      suite_name = buildable_reference.attributes['BlueprintName']
      if (change_reason = changes_by_suite_name[suite_name])
        puts "#{suite_name} changed because #{change_reason}" if log_changes
        each_target&.call(type: :changed, target_name: suite_name, change_reason: change_reason)
        next
      end
      puts "#{suite_name} did not change, removing from scheme" if log_changes
      each_target&.call(type: :unchanged, target_name: suite_name, change_reason: nil)
      buildable_reference.parent.remove
    end
  end

  if filter_scheme_for_build_action == :testing
    doc.get_elements('//*/BuildActionEntry/BuildableReference').to_a.each do |buildable_reference|
      suite_name = buildable_reference.attributes['BlueprintName']
      if (change_reason = changes_by_suite_name[suite_name])
        puts "#{suite_name} changed because #{change_reason}" if log_changes
        each_target&.call(type: :changed, target_name: suite_name, change_reason: change_reason)
        next
      end
      puts "#{suite_name} did not change, setting to not build for testing" if log_changes
      each_target&.call(type: :unchanged, target_name: suite_name, change_reason: nil)
      buildable_reference.parent.attributes['buildForTesting'] = 'NO'
    end
  end

  scheme
end

#format_changes(include_unchanged_targets: false, change_level: :full_transitive) ⇒ String

Returns a string suitable for user display that explains target changes.

Parameters:

  • include_unchanged_targets (Boolean) (defaults to: false)

    whether targets that have not changed should also be displayed

  • change_level (Symbol) (defaults to: :full_transitive)

    the change level used for computing whether a target has changed

Returns:

  • (String)

    a string suitable for user display that explains target changes



126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/refinement/analyzer.rb', line 126

def format_changes(include_unchanged_targets: false, change_level: :full_transitive)
  annotate_targets!.group_by { |target| target.xcode_target.project.path.to_s }.sort_by(&:first)
                   .map do |project, annotated_targets|
    changes = annotated_targets.sort_by { |annotated_target| annotated_target.xcode_target.name }
                               .map do |annotated_target|
      change_reason = annotated_target.change_reason(level: change_level)
      next if !include_unchanged_targets && !change_reason

      change_reason ||= 'did not change'
      "\t#{annotated_target.xcode_target}: #{change_reason}"
    end.compact
    "#{project}:\n#{changes.join("\n")}" unless changes.empty?
  end.compact.join("\n")
end