Class: Homebrew::TestBot::Formulae Private

Inherits:
TestFormulae show all
Includes:
OS::Mac::TestBot::Formulae
Defined in:
test_bot/formulae.rb

This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.

Instance Attribute Summary collapse

Attributes inherited from TestFormulae

#artifact_cache, #downloaded_artifacts, #skipped_or_failed_formulae

Attributes inherited from Test

#steps

Instance Method Summary collapse

Methods inherited from TestFormulae

#download_artifacts_from_previous_run!

Methods included from OS::Linux::TestBot::TestFormulae

#previous_run_artifact_specifier

Methods included from OS::Mac::TestBot::TestFormulae

#previous_run_artifact_specifier

Methods inherited from Test

#failed_steps, #ignored_steps, #test

Methods included from Utils::Output::Mixin

#issue_reporting_message, #odebug, #odeprecated, #odie, #odisabled, #ofail, #oh1, #oh1_title, #ohai, #ohai_title, #onoe, #opoo, #opoo_outside_github_actions, #opoo_without_github_actions_annotation, #pretty_deprecated, #pretty_disabled, #pretty_duration, #pretty_install_status, #pretty_installed, #pretty_uninstalled, #pretty_unmarked, #pretty_upgradable, #pretty_warning

Constructor Details

#initialize(tap:, git:, dry_run:, fail_fast:, verbose:, output_paths:) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:



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

def initialize(tap:, git:, dry_run:, fail_fast:, verbose:, output_paths:)
  super(tap:, git:, dry_run:, fail_fast:, verbose:)

  @built_formulae = T.let([], T::Array[String])
  @bottle_checksums = T.let({}, T::Hash[Pathname, String])
  @bottle_output_path = T.let(output_paths.fetch(:bottle), Pathname)
  @linkage_output_path = T.let(output_paths.fetch(:linkage), Pathname)
  @skipped_or_failed_formulae_output_path = T.let(output_paths.fetch(:skipped_or_failed_formulae), Pathname)
  @testing_formulae = T.let([], T::Array[String])
  @added_formulae = T.let([], T::Array[String])
  @deleted_formulae = T.let([], T::Array[String])
  @testing_formulae_count = T.let(0, Integer)
  @tested_formulae_count = T.let(0, Integer)
  @unchanged_dependencies = T.let([], T::Array[String])
  @unchanged_build_dependencies = T.let([], T::Array[String])
  @bottle_filename = T.let(nil, T.nilable(Pathname))
  @bottle_json_filename = T.let(nil, T.nilable(Pathname))
end

Instance Attribute Details

#added_formulae=(value) ⇒ void (writeonly)

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



11
12
13
# File 'test_bot/formulae.rb', line 11

def added_formulae=(value)
  @added_formulae = value
end

#deleted_formulae=(value) ⇒ void (writeonly)

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



14
15
16
# File 'test_bot/formulae.rb', line 14

def deleted_formulae=(value)
  @deleted_formulae = value
end

#testing_formulae=(value) ⇒ void (writeonly)

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



8
9
10
# File 'test_bot/formulae.rb', line 8

def testing_formulae=(value)
  @testing_formulae = value
end

Instance Method Details

#annotate_added_dependencies(formula) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



175
176
177
178
179
180
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'test_bot/formulae.rb', line 175

def annotate_added_dependencies(formula)
  return unless GitHub::Actions.env_set?
  return if @added_formulae.include?(formula.name)
  return if (git = self.git).nil?

  direct_runtime_dependencies = formula.deps.reject do |dependency|
    dependency.build? || dependency.optional? || dependency.test?
  end

  new_line = T.let(nil, T.nilable(Integer))
  Utils.safe_popen_read(
    git, "-C", repository, "diff", "--no-ext-diff", "--unified=0", "origin/HEAD", "HEAD", "--",
    formula.path.relative_path_from(repository).to_s
  ).each_line do |diff_line|
    if (match = diff_line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/))
      new_line = match[1]&.to_i
      next
    end

    line = new_line
    next if line.nil?

    if diff_line.start_with?("+") && !diff_line.start_with?("+++")
      dependency_name = diff_line[/^\+\s*depends_on\s+["']([^"']+)["']/, 1]
      new_line = line + 1
    elsif !diff_line.start_with?("-") || diff_line.start_with?("---")
      new_line = line + 1
      next
    else
      next
    end
    next if dependency_name.blank?

    dependency = direct_runtime_dependencies.find do |runtime_dependency|
      dependency_name_match?(runtime_dependency, dependency_name)
    end
    next if dependency.nil?

    dependency_formula = dependency.to_formula
    existing_runtime_dependency_names = recursive_runtime_dependency_names(
      formula,
      direct_runtime_dependencies.reject do |runtime_dependency|
        dependency_name_match?(runtime_dependency, dependency.name)
      end,
    )
    new_recursive_dependency_names =
      (
        [dependency_formula.full_name] +
        recursive_runtime_dependency_names(
          dependency_formula,
          dependency_formula.runtime_dependencies(read_from_tab: false, undeclared: false),
        )
      ).uniq - existing_runtime_dependency_names
    next if new_recursive_dependency_names.blank?

    sizes = new_recursive_dependency_names.map do |formula_name|
      installed_sizes = @dependency_impact_installed_sizes ||=
        T.let({}, T.nilable(T::Hash[String, T.nilable(Integer)]))
      next installed_sizes[formula_name] if installed_sizes.key?(formula_name)

      installed_sizes[formula_name] =
        if (bottle = Formulary.factory(formula_name).bottle_for_tag(Utils::Bottles.tag))
          bottle.fetch_tab(quiet: true)
          bottle.installed_size
        end
    rescue DownloadError, FormulaUnavailableError, Resource::BottleManifest::Error
      nil
    end
    dependency_count = new_recursive_dependency_names.count
    message = "Adding `#{dependency_name}` adds #{dependency_count} new recursive " \
              "#{Utils.pluralize("dependency", dependency_count)} " \
              "on #{Utils::Bottles.tag} (#{Formatter.disk_usage_readable(sizes.compact.sum)}"
    unknown_size_count = sizes.count(&:nil?)
    message << ", plus #{Utils.pluralize("unknown size", unknown_size_count, include_count: true)}" \
      if unknown_size_count.positive?
    puts GitHub::Actions::Annotation.new(
      :warning,
      "#{message}).",
      file:  formula.path.to_s.delete_prefix("#{repository}/"),
      line:,
      title: "#{formula}: new dependency impact",
    )
  end
rescue => e
  opoo "Failed to determine dependency impact for #{formula.full_name}: #{e}"
end

#annotate_missing_all_bottle(formula, bottle_dir: Pathname.pwd) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'test_bot/formulae.rb', line 263

def annotate_missing_all_bottle(formula, bottle_dir: Pathname.pwd)
  return unless formula.bottle_specification.tag?(Utils::Bottles.tag(:all))

  require "utils/ast"

  bottle_tag = Utils::Bottles.tag
  bottle_sha256_node_tag = lambda do |sha256_node, tag|
    sha256_node.arguments.grep(RuboCop::AST::HashNode).any? do |hash_node|
      hash_node.pairs.any? do |pair|
        Utils::AST.literal_value(pair.key) == tag ||
          Utils::AST.literal_value(pair.value) == tag
      end
    end
  end
  bottle_node = Utils::AST::FormulaAST.new(formula.path.read).bottle_block
  sha256_nodes = Utils::AST.body_children(
    bottle_node.is_a?(RuboCop::AST::BlockNode) ? bottle_node.body : nil,
  ).filter_map do |node|
    next unless node.is_a?(RuboCop::AST::SendNode)
    next if node.method_name != :sha256

    node
  end
  return if sha256_nodes.any? { |sha256_node| bottle_sha256_node_tag.call(sha256_node, :all) }

  # This predictor only has local JSONs, so mirror the merge-time tag count
  # and cellar/checksum dedupe gates; final merge handles version/rebuild.
  local_tag_hashes = local_bottle_tag_hashes(formula.name, bottle_dir:)
  return if local_tag_hashes.key?("all")
  return if local_tag_hashes.count < 2
  return if local_tag_hashes.values.uniq { |tag_hash| [tag_hash["cellar"], tag_hash["sha256"]] }.one?

  tag_hash = local_tag_hashes[bottle_tag.to_s]
  line = sha256_nodes.find do |sha256_node|
    bottle_sha256_node_tag.call(sha256_node, bottle_tag.to_sym)
  end&.source_range&.line
  bottle_details = if tag_hash.present?
    " (cellar `#{tag_hash["cellar"]}`, sha256 `#{tag_hash["sha256"]}`)"
  else
    ""
  end
  message = "This formula had an `:all` bottle but the #{bottle_tag} test-bot bottle is " \
            "platform-specific#{bottle_details}. If the final bottle merge cannot create a new " \
            "`:all` bottle, expect #{Utils::Bottles.missing_all_bottle_publish_note}; this is for " \
            "information only and should not block merge."

  if GitHub::Actions.env_set?
    puts GitHub::Actions::Annotation.new(
      :warning,
      message,
      file:  formula.path.to_s.delete_prefix("#{repository}/"),
      line:,
      title: "#{formula}: missing :all bottle",
    )
  else
    opoo message
  end
rescue => e
  opoo "Failed to determine missing `:all` bottle impact for #{formula.full_name}: #{e}"
end

#cleanup_bottle_etc_var(formula) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



109
110
111
112
113
# File 'test_bot/formulae.rb', line 109

def cleanup_bottle_etc_var(formula)
  # Restore bottled `etc`/`var` through `Formula#install_etc_var`, keeping
  # test-bot cleanup aligned with `InstallRenamed` config handling.
  formula.install_etc_var
end

#dependency_name_match?(dependency, dependency_name) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:

  • (Boolean)


166
167
168
169
170
171
172
# File 'test_bot/formulae.rb', line 166

def dependency_name_match?(dependency, dependency_name)
  return true if dependency.name == dependency_name
  return false if Utils.tap_from_full_name(dependency.name).present?
  return false if Utils.tap_from_full_name(dependency_name).present?

  Utils.name_from_full_name(dependency.name) == Utils.name_from_full_name(dependency_name)
end

#run!(args:) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
# File 'test_bot/formulae.rb', line 46

def run!(args:)
  test_header(:Formulae)

  verify_local_bottles

  with_env(HOMEBREW_DISABLE_LOAD_FORMULA: "1") do
    # Portable Ruby bottles are handled differently.
    next if testing_portable_ruby?

    download_artifacts_from_previous_run!("bottles{,_#{previous_run_artifact_specifier}*}",
                                          dry_run: args.dry_run?)
  end
  @bottle_checksums.merge!(
    bottle_glob("*", artifact_cache, ".{json,tar.gz}", bottle_tag: "*").to_h do |bottle_file|
      [bottle_file.realpath, bottle_file.sha256]
    end,
  )

  # #run! modifies `@testing_formulae`, so we need to track this separately.
  @testing_formulae_count = @testing_formulae.count
  perform_bash_cleanup = @testing_formulae.include?("bash")
  @tested_formulae_count = 0

  sorted_formulae.each do |f|
    verify_local_bottles
    if testing_portable_ruby?
      portable_formula!(f, args:)
    else
      formula!(f, args:)
    end
    puts
    next if @testing_formulae_count < 3

    progress_text = +"Test progress: "
    progress_text += "#{@tested_formulae_count} formula(e) tested, "
    progress_text += "#{@testing_formulae_count - @tested_formulae_count} remaining"
    info_header progress_text
  end

  @deleted_formulae.each do |f|
    deleted_formula!(f)
    verify_local_bottles
    puts
  end

  return unless GitHub::Actions.env_set?

  # Remove `bash` after it is tested, since leaving a broken `bash`
  # installation in the environment can cause issues with subsequent
  # GitHub Actions steps.
  test "brew", "uninstall", "--formula", "--force", "bash" if perform_bash_cleanup

  File.open(ENV.fetch("GITHUB_OUTPUT"), "a") do |f|
    f.puts "skipped_or_failed_formulae=#{@skipped_or_failed_formulae.join(",")}"
  end

  @skipped_or_failed_formulae_output_path.write(@skipped_or_failed_formulae.join(","))
ensure
  verify_local_bottles
  FileUtils.rm_rf artifact_cache
end

#testing_portable_ruby?Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:

  • (Boolean)


325
326
327
# File 'test_bot/formulae.rb', line 325

def testing_portable_ruby?
  !!tap&.core_tap? && @testing_formulae.include?("portable-ruby")
end

#verify_local_bottlesBoolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:

  • (Boolean)


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'test_bot/formulae.rb', line 116

def verify_local_bottles
  # Portable Ruby bottles are handled differently.
  return false if testing_portable_ruby?

  # Setting `HOMEBREW_DISABLE_LOAD_FORMULA` probably doesn't do anything here but let's set it just to be safe.
  with_env(HOMEBREW_DISABLE_LOAD_FORMULA: "1") do
    missing_bottles = @bottle_checksums.keys.reject do |bottle_path|
      next true if bottle_path.exist?

      what = (bottle_path.extname == ".json") ? "JSON" : "tarball"
      onoe "Missing bottle #{what}: #{bottle_path}"
      false
    end

    mismatched_checksums = @bottle_checksums.reject do |bottle_path, expected_sha256|
      next true unless bottle_path.exist?
      next true if (actual_sha256 = bottle_path.sha256) == expected_sha256

      onoe <<~ERROR
        Bottle checksum mismatch for #{bottle_path}!
          Expected: #{expected_sha256}
          Actual:   #{actual_sha256}
      ERROR
      false
    end

    unexpected_bottles = bottle_glob(
      "**/*", Pathname.pwd, ".{json,tar.gz}", bottle_tag: "*"
    ).reject do |local_bottle|
      next true if @bottle_checksums.key?(local_bottle.realpath)

      what = (local_bottle.extname == ".json") ? "JSON" : "tarball"
      onoe "Unexpected bottle #{what}: #{local_bottle}"
      false
    end

    return true if missing_bottles.blank? && mismatched_checksums.blank? && unexpected_bottles.blank?

    # Delete these files so we don't end up uploading them.
    files_to_delete = mismatched_checksums.keys + unexpected_bottles
    files_to_delete += files_to_delete.select(&:symlink?).map(&:realpath)
    FileUtils.rm_rf files_to_delete

    test "false" # ensure that `test-bot` exits with an error.

    false
  end
end