mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

* test-bot: start running generic tests. Start running the test suite in the "generic" mode i.e. a base layer for non-OS X platforms to be able to use to ensure we don't break the generic code for the parts of the code we've got running. Currently this just runs the integration tests as that's the only useful suite that's entirely passing but eventually this will be changed to run the full test suite in generic mode. * test_integration_cmds: fix tests on Linux.
1048 lines
36 KiB
Ruby
1048 lines
36 KiB
Ruby
# Comprehensively test a formula or pull request.
|
|
#
|
|
# Usage: brew test-bot [options...] <pull-request|formula>
|
|
#
|
|
# Options:
|
|
# --keep-logs: Write and keep log files under ./brewbot/.
|
|
# --cleanup: Clean the Homebrew directory. Very dangerous. Use with care.
|
|
# --clean-cache: Remove all cached downloads. Use with care.
|
|
# --skip-setup: Don't check the local system is setup correctly.
|
|
# --skip-homebrew: Don't check Homebrew's files and tests are all valid.
|
|
# --junit: Generate a JUnit XML test results file.
|
|
# --no-bottle: Run brew install without --build-bottle.
|
|
# --keep-old: Run brew bottle --keep-old to build new bottles for a single platform.
|
|
# --skip-relocation: Run brew bottle --skip-relocation to build new bottles for homebrew/portable.
|
|
# --HEAD: Run brew install with --HEAD.
|
|
# --local: Ask Homebrew to write verbose logs under ./logs/ and set HOME to ./home/.
|
|
# --tap=<tap>: Use the git repository of the given tap.
|
|
# --dry-run: Just print commands, don't run them.
|
|
# --fail-fast: Immediately exit on a failing step.
|
|
# --verbose: Print test step output in realtime. Has the side effect of passing output
|
|
# as raw bytes instead of re-encoding in UTF-8.
|
|
# --fast: Don't install any packages, but run e.g. audit anyway.
|
|
# --keep-tmp: Keep temporary files written by main installs and tests that are run.
|
|
# --no-pull Don't use `brew pull` when possible.
|
|
#
|
|
# --ci-master: Shortcut for Homebrew master branch CI options.
|
|
# --ci-pr: Shortcut for Homebrew pull request CI options.
|
|
# --ci-testing: Shortcut for Homebrew testing CI options.
|
|
# --ci-upload: Homebrew CI bottle upload.
|
|
#
|
|
# Influential environment variables include:
|
|
# TRAVIS_REPO_SLUG: same as --tap
|
|
# GIT_URL: if set to URL of a tap remote, same as --tap
|
|
|
|
require "formula"
|
|
require "utils"
|
|
require "date"
|
|
require "rexml/document"
|
|
require "rexml/xmldecl"
|
|
require "rexml/cdata"
|
|
require "tap"
|
|
|
|
module Homebrew
|
|
BYTES_IN_1_MEGABYTE = 1024*1024
|
|
MAX_STEP_OUTPUT_SIZE = BYTES_IN_1_MEGABYTE - (200*1024) # margin of safety
|
|
|
|
HOMEBREW_TAP_REGEX = %r{^([\w-]+)/homebrew-([\w-]+)$}
|
|
|
|
if ruby_has_encoding?
|
|
def fix_encoding!(str)
|
|
# Assume we are starting from a "mostly" UTF-8 string
|
|
str.force_encoding(Encoding::UTF_8)
|
|
return str if str.valid_encoding?
|
|
str.encode!(Encoding::UTF_16, :invalid => :replace)
|
|
str.encode!(Encoding::UTF_8)
|
|
end
|
|
elsif require "iconv"
|
|
def fix_encoding!(str)
|
|
Iconv.conv("UTF-8//IGNORE", "UTF-8", str)
|
|
end
|
|
else
|
|
def fix_encoding!(str)
|
|
str
|
|
end
|
|
end
|
|
|
|
def resolve_test_tap
|
|
if tap = ARGV.value("tap")
|
|
return Tap.fetch(tap)
|
|
end
|
|
|
|
if (tap = ENV["TRAVIS_REPO_SLUG"]) && (tap =~ HOMEBREW_TAP_REGEX)
|
|
return Tap.fetch(tap)
|
|
end
|
|
|
|
if ENV["UPSTREAM_BOT_PARAMS"]
|
|
bot_argv = ENV["UPSTREAM_BOT_PARAMS"].split " "
|
|
bot_argv.extend HomebrewArgvExtension
|
|
if tap = bot_argv.value("tap")
|
|
return Tap.fetch(tap)
|
|
end
|
|
end
|
|
|
|
if git_url = ENV["UPSTREAM_GIT_URL"] || ENV["GIT_URL"]
|
|
# Also can get tap from Jenkins GIT_URL.
|
|
url_path = git_url.sub(%r{^https?://github\.com/}, "").chomp("/").sub(%r{\.git$}, "")
|
|
begin
|
|
return Tap.fetch(url_path) if url_path =~ HOMEBREW_TAP_REGEX
|
|
rescue
|
|
end
|
|
end
|
|
end
|
|
|
|
# Wraps command invocations. Instantiated by Test#test.
|
|
# Handles logging and pretty-printing.
|
|
class Step
|
|
attr_reader :command, :name, :status, :output
|
|
|
|
# Instantiates a Step object.
|
|
# @param test [Test] The parent Test object
|
|
# @param command [Array<String>] Command to execute and arguments
|
|
# @param options [Hash] Recognized options are:
|
|
# :puts_output_on_success
|
|
# :repository
|
|
def initialize(test, command, options = {})
|
|
@test = test
|
|
@category = test.category
|
|
@command = command
|
|
@puts_output_on_success = options[:puts_output_on_success]
|
|
@name = command[1].delete("-")
|
|
@status = :running
|
|
@repository = options[:repository] || HOMEBREW_REPOSITORY
|
|
end
|
|
|
|
def log_file_path
|
|
file = "#{@category}.#{@name}.txt"
|
|
root = @test.log_root
|
|
root ? root + file : file
|
|
end
|
|
|
|
def command_short
|
|
(@command - %w[brew --force --retry --verbose --build-bottle --json]).join(" ")
|
|
end
|
|
|
|
def passed?
|
|
@status == :passed
|
|
end
|
|
|
|
def failed?
|
|
@status == :failed
|
|
end
|
|
|
|
def puts_command
|
|
if ENV["TRAVIS"]
|
|
@@travis_step_num ||= 0
|
|
@travis_fold_id = @command.first(2).join(".") + ".#{@@travis_step_num += 1}"
|
|
@travis_timer_id = rand(2**32).to_s(16)
|
|
puts "travis_fold:start:#{@travis_fold_id}"
|
|
puts "travis_time:start:#{@travis_timer_id}"
|
|
end
|
|
puts "#{Tty.blue}==>#{Tty.white} #{@command.join(" ")}#{Tty.reset}"
|
|
end
|
|
|
|
def puts_result
|
|
if ENV["TRAVIS"]
|
|
travis_start_time = @start_time.to_i*1000000000
|
|
travis_end_time = @end_time.to_i*1000000000
|
|
travis_duration = travis_end_time - travis_start_time
|
|
puts "#{Tty.white}==>#{Tty.green} PASSED#{Tty.reset}" if passed?
|
|
puts "travis_time:end:#{@travis_timer_id},start=#{travis_start_time},finish=#{travis_end_time},duration=#{travis_duration}"
|
|
puts "travis_fold:end:#{@travis_fold_id}"
|
|
end
|
|
puts "#{Tty.white}==>#{Tty.red} FAILED#{Tty.reset}" if failed?
|
|
end
|
|
|
|
def has_output?
|
|
@output && !@output.empty?
|
|
end
|
|
|
|
# The execution time of the task.
|
|
# Precondition: Step#run has been called.
|
|
# @return [Float] execution time in seconds
|
|
def time
|
|
@end_time - @start_time
|
|
end
|
|
|
|
def run
|
|
@start_time = Time.now
|
|
|
|
puts_command
|
|
if ARGV.include? "--dry-run"
|
|
@end_time = Time.now
|
|
@status = :passed
|
|
puts_result
|
|
return
|
|
end
|
|
|
|
verbose = ARGV.verbose?
|
|
# Step may produce arbitrary output and we read it bytewise, so must
|
|
# buffer it as binary and convert to UTF-8 once complete
|
|
output = ruby_has_encoding? ? "".encode!("BINARY") : ""
|
|
working_dir = Pathname.new(@command.first == "git" ? @repository : Dir.pwd)
|
|
read, write = IO.pipe
|
|
|
|
begin
|
|
pid = fork do
|
|
read.close
|
|
$stdout.reopen(write)
|
|
$stderr.reopen(write)
|
|
write.close
|
|
working_dir.cd { exec(*@command) }
|
|
end
|
|
write.close
|
|
while buf = read.readpartial(4096)
|
|
if verbose
|
|
print buf
|
|
$stdout.flush
|
|
end
|
|
output << buf
|
|
end
|
|
rescue EOFError
|
|
ensure
|
|
read.close
|
|
end
|
|
|
|
Process.wait(pid)
|
|
@end_time = Time.now
|
|
@status = $?.success? ? :passed : :failed
|
|
puts_result
|
|
|
|
|
|
unless output.empty?
|
|
@output = Homebrew.fix_encoding!(output)
|
|
puts @output if (failed? || @puts_output_on_success) && !verbose
|
|
File.write(log_file_path, @output) if ARGV.include? "--keep-logs"
|
|
end
|
|
|
|
exit 1 if ARGV.include?("--fail-fast") && failed?
|
|
end
|
|
end
|
|
|
|
class Test
|
|
attr_reader :log_root, :category, :name, :steps
|
|
|
|
def initialize(argument, options={})
|
|
@hash = nil
|
|
@url = nil
|
|
@formulae = []
|
|
@added_formulae = []
|
|
@modified_formula = []
|
|
@steps = []
|
|
@tap = options[:tap]
|
|
@repository = @tap ? @tap.path : HOMEBREW_REPOSITORY
|
|
@skip_homebrew = options.fetch(:skip_homebrew, false)
|
|
|
|
if quiet_system "git", "-C", @repository.to_s, "rev-parse", "--verify", "-q", argument
|
|
@hash = argument
|
|
elsif url_match = argument.match(HOMEBREW_PULL_OR_COMMIT_URL_REGEX)
|
|
@url = url_match[0]
|
|
elsif canonical_formula_name = safe_formula_canonical_name(argument)
|
|
@formulae = [canonical_formula_name]
|
|
else
|
|
raise ArgumentError.new("#{argument} is not a pull request URL, commit URL or formula name.")
|
|
end
|
|
|
|
@category = __method__
|
|
@brewbot_root = Pathname.pwd + "brewbot"
|
|
FileUtils.mkdir_p @brewbot_root
|
|
end
|
|
|
|
def no_args?
|
|
@hash == "HEAD"
|
|
end
|
|
|
|
def safe_formula_canonical_name(formula_name)
|
|
Formulary.factory(formula_name).full_name
|
|
rescue TapFormulaUnavailableError => e
|
|
raise if e.tap.installed?
|
|
test "brew", "tap", e.tap.name
|
|
retry unless steps.last.failed?
|
|
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
|
|
end
|
|
|
|
def git(*args)
|
|
@repository.cd { Utils.popen_read("git", *args) }
|
|
end
|
|
|
|
def download
|
|
def shorten_revision(revision)
|
|
git("rev-parse", "--short", revision).strip
|
|
end
|
|
|
|
def current_sha1
|
|
shorten_revision "HEAD"
|
|
end
|
|
|
|
def current_branch
|
|
git("symbolic-ref", "HEAD").gsub("refs/heads/", "").strip
|
|
end
|
|
|
|
def single_commit?(start_revision, end_revision)
|
|
git("rev-list", "--count", "#{start_revision}..#{end_revision}").to_i == 1
|
|
end
|
|
|
|
def diff_formulae(start_revision, end_revision, path, filter)
|
|
return unless @tap
|
|
git(
|
|
"diff-tree", "-r", "--name-only", "--diff-filter=#{filter}",
|
|
start_revision, end_revision, "--", path
|
|
).lines.map do |line|
|
|
file = Pathname.new line.chomp
|
|
next unless @tap.formula_file?(file)
|
|
@tap.formula_file_to_name(file)
|
|
end.compact
|
|
end
|
|
|
|
@category = __method__
|
|
@start_branch = current_branch
|
|
|
|
travis_pr = ENV["TRAVIS_PULL_REQUEST"] && ENV["TRAVIS_PULL_REQUEST"] != "false"
|
|
|
|
# Use Jenkins GitHub Pull Request Builder plugin variables for
|
|
# pull request jobs.
|
|
if ENV["ghprbPullLink"]
|
|
@url = ENV["ghprbPullLink"]
|
|
@hash = nil
|
|
test "git", "checkout", "origin/master"
|
|
elsif ENV["GIT_URL"] && ENV["GIT_BRANCH"]
|
|
git_url = ENV["GIT_URL"].chomp("/").chomp(".git")
|
|
%r{origin/pr/(\d+)/(merge|head)} =~ ENV["GIT_BRANCH"]
|
|
pr = $1
|
|
@url = "#{git_url}/pull/#{pr}"
|
|
@hash = nil
|
|
# Use Travis CI pull-request variables for pull request jobs.
|
|
elsif travis_pr
|
|
@url = "https://github.com/#{ENV["TRAVIS_REPO_SLUG"]}/pull/#{ENV["TRAVIS_PULL_REQUEST"]}"
|
|
@hash = nil
|
|
end
|
|
|
|
# Use Jenkins Git plugin variables for master branch jobs.
|
|
if ENV["GIT_PREVIOUS_COMMIT"] && ENV["GIT_COMMIT"]
|
|
diff_start_sha1 = ENV["GIT_PREVIOUS_COMMIT"]
|
|
diff_end_sha1 = ENV["GIT_COMMIT"]
|
|
# Use Travis CI Git variables for master or branch jobs.
|
|
elsif ENV["TRAVIS_COMMIT_RANGE"]
|
|
diff_start_sha1, diff_end_sha1 = ENV["TRAVIS_COMMIT_RANGE"].split "..."
|
|
# Otherwise just use the current SHA-1 (which may be overriden later)
|
|
else
|
|
diff_end_sha1 = diff_start_sha1 = current_sha1
|
|
end
|
|
|
|
diff_start_sha1 = git("merge-base", diff_start_sha1, diff_end_sha1).strip
|
|
|
|
# Handle no arguments being passed on the command-line e.g. `brew test-bot`.
|
|
if no_args?
|
|
if diff_start_sha1 == diff_end_sha1 || \
|
|
single_commit?(diff_start_sha1, diff_end_sha1)
|
|
@name = diff_end_sha1
|
|
else
|
|
@name = "#{diff_start_sha1}-#{diff_end_sha1}"
|
|
end
|
|
# Handle formulae arguments being passed on the command-line e.g. `brew test-bot wget fish`.
|
|
elsif @formulae && @formulae.any?
|
|
@name = "#{@formulae.first}-#{diff_end_sha1}"
|
|
diff_start_sha1 = diff_end_sha1
|
|
# Handle a hash being passed on the command-line e.g. `brew test-bot 1a2b3c`.
|
|
elsif @hash
|
|
test "git", "checkout", @hash
|
|
diff_start_sha1 = "#{@hash}^"
|
|
diff_end_sha1 = @hash
|
|
@name = @hash
|
|
# Handle a URL being passed on the command-line or through Jenkins/Travis
|
|
# environment variables e.g.
|
|
# `brew test-bot https://github.com/Homebrew/homebrew-core/pull/678`.
|
|
elsif @url
|
|
# TODO: in future Travis CI may need to also use `brew pull` to e.g. push
|
|
# the right commit to BrewTestBot.
|
|
if !travis_pr && !ARGV.include?("--no-pull")
|
|
diff_start_sha1 = current_sha1
|
|
test "brew", "pull", "--clean", @url
|
|
diff_end_sha1 = current_sha1
|
|
end
|
|
@short_url = @url.gsub("https://github.com/", "")
|
|
if @short_url.include? "/commit/"
|
|
# 7 characters should be enough for a commit (not 40).
|
|
@short_url.gsub!(/(commit\/\w{7}).*/, '\1')
|
|
@name = @short_url
|
|
else
|
|
@name = "#{@short_url}-#{diff_end_sha1}"
|
|
end
|
|
else
|
|
raise "Cannot set @name: invalid command-line arguments!"
|
|
end
|
|
|
|
@log_root = @brewbot_root + @name
|
|
FileUtils.mkdir_p @log_root
|
|
|
|
return unless diff_start_sha1 != diff_end_sha1
|
|
return if @url && steps.last && !steps.last.passed?
|
|
return unless @tap
|
|
|
|
formula_path = @tap.formula_dir.to_s
|
|
@added_formulae += diff_formulae(diff_start_sha1, diff_end_sha1, formula_path, "A")
|
|
@modified_formula += diff_formulae(diff_start_sha1, diff_end_sha1, formula_path, "M")
|
|
@formulae += @added_formulae + @modified_formula
|
|
end
|
|
|
|
def skip(formula_name)
|
|
puts "#{Tty.blue}==>#{Tty.white} SKIPPING: #{formula_name}#{Tty.reset}"
|
|
end
|
|
|
|
def satisfied_requirements?(formula, spec, dependency = nil)
|
|
requirements = formula.send(spec).requirements
|
|
|
|
unsatisfied_requirements = requirements.reject do |requirement|
|
|
satisfied = false
|
|
satisfied ||= requirement.satisfied?
|
|
satisfied ||= requirement.optional?
|
|
if !satisfied && requirement.default_formula?
|
|
default = Formula[requirement.default_formula]
|
|
satisfied = satisfied_requirements?(default, :stable, formula.full_name)
|
|
end
|
|
satisfied
|
|
end
|
|
|
|
if unsatisfied_requirements.empty?
|
|
true
|
|
else
|
|
name = formula.full_name
|
|
name += " (#{spec})" unless spec == :stable
|
|
name += " (#{dependency} dependency)" if dependency
|
|
skip name
|
|
puts unsatisfied_requirements.map(&:message)
|
|
false
|
|
end
|
|
end
|
|
|
|
def setup
|
|
@category = __method__
|
|
return if ARGV.include? "--skip-setup"
|
|
if !ENV["TRAVIS"] && ENV["HOMEBREW_RUBY"] != "1.8.7" &&
|
|
HOMEBREW_PREFIX.to_s == "/usr/local"
|
|
test "brew", "doctor"
|
|
end
|
|
test "brew", "--env"
|
|
test "brew", "config"
|
|
end
|
|
|
|
def formula(formula_name)
|
|
@category = "#{__method__}.#{formula_name}"
|
|
|
|
test "brew", "uses", formula_name
|
|
|
|
formula = Formulary.factory(formula_name)
|
|
|
|
installed_gcc = false
|
|
|
|
deps = []
|
|
reqs = []
|
|
|
|
fetch_args = [formula_name]
|
|
fetch_args << "--build-bottle" if !ARGV.include?("--fast") && !ARGV.include?("--no-bottle") && !formula.bottle_disabled?
|
|
fetch_args << "--force" if ARGV.include? "--cleanup"
|
|
|
|
audit_args = [formula_name]
|
|
audit_args << "--strict" << "--online" if @added_formulae.include? formula_name
|
|
|
|
if formula.stable
|
|
unless satisfied_requirements?(formula, :stable)
|
|
test "brew", "fetch", "--retry", *fetch_args
|
|
test "brew", "audit", *audit_args
|
|
return
|
|
end
|
|
|
|
deps |= formula.stable.deps.to_a.reject(&:optional?)
|
|
reqs |= formula.stable.requirements.to_a.reject(&:optional?)
|
|
elsif formula.devel
|
|
unless satisfied_requirements?(formula, :devel)
|
|
test "brew", "fetch", "--retry", "--devel", *fetch_args
|
|
test "brew", "audit", "--devel", *audit_args
|
|
return
|
|
end
|
|
end
|
|
|
|
if formula.devel && !ARGV.include?("--HEAD")
|
|
deps |= formula.devel.deps.to_a.reject(&:optional?)
|
|
reqs |= formula.devel.requirements.to_a.reject(&:optional?)
|
|
end
|
|
|
|
begin
|
|
deps.each { |d| d.to_formula.recursive_dependencies }
|
|
rescue TapFormulaUnavailableError => e
|
|
raise if e.tap.installed?
|
|
safe_system "brew", "tap", e.tap.name
|
|
retry
|
|
end
|
|
|
|
begin
|
|
deps.each do |dep|
|
|
CompilerSelector.select_for(dep.to_formula)
|
|
end
|
|
CompilerSelector.select_for(formula)
|
|
rescue CompilerSelectionError => e
|
|
unless installed_gcc
|
|
run_as_not_developer { test "brew", "install", "gcc" }
|
|
installed_gcc = true
|
|
DevelopmentTools.clear_version_cache
|
|
retry
|
|
end
|
|
skip formula_name
|
|
puts e.message
|
|
return
|
|
end
|
|
|
|
conflicts = formula.conflicts
|
|
formula.recursive_dependencies.each do |dependency|
|
|
conflicts += dependency.to_formula.conflicts
|
|
end
|
|
|
|
conflicts.each do |conflict|
|
|
confict_formula = Formulary.factory(conflict.name)
|
|
|
|
if confict_formula.installed? && confict_formula.linked_keg.exist?
|
|
test "brew", "unlink", "--force", conflict.name
|
|
end
|
|
end
|
|
|
|
installed = Utils.popen_read("brew", "list").split("\n")
|
|
dependencies = Utils.popen_read("brew", "deps", "--include-build", formula_name).split("\n")
|
|
|
|
(installed & dependencies).each do |installed_dependency|
|
|
installed_dependency_formula = Formulary.factory(installed_dependency)
|
|
if installed_dependency_formula.installed? &&
|
|
!installed_dependency_formula.keg_only? &&
|
|
!installed_dependency_formula.linked_keg.exist?
|
|
test "brew", "link", installed_dependency
|
|
end
|
|
end
|
|
|
|
dependencies -= installed
|
|
unchanged_dependencies = dependencies - @formulae
|
|
changed_dependences = dependencies - unchanged_dependencies
|
|
|
|
runtime_dependencies = Utils.popen_read("brew", "deps", formula_name).split("\n")
|
|
build_dependencies = dependencies - runtime_dependencies
|
|
unchanged_build_dependencies = build_dependencies - @formulae
|
|
|
|
dependents = Utils.popen_read("brew", "uses", formula_name).split("\n")
|
|
dependents -= @formulae
|
|
dependents = dependents.map { |d| Formulary.factory(d) }
|
|
|
|
bottled_dependents = dependents.select { |d| d.bottled? }
|
|
testable_dependents = dependents.select { |d| d.bottled? && d.test_defined? }
|
|
|
|
if (deps | reqs).any? { |d| d.name == "mercurial" && d.build? }
|
|
run_as_not_developer { test "brew", "install", "mercurial" }
|
|
end
|
|
|
|
test "brew", "fetch", "--retry", *unchanged_dependencies unless unchanged_dependencies.empty?
|
|
|
|
unless changed_dependences.empty?
|
|
test "brew", "fetch", "--retry", "--build-bottle", *changed_dependences
|
|
unless ARGV.include?("--fast")
|
|
# Install changed dependencies as new bottles so we don't have checksum problems.
|
|
test "brew", "install", "--build-bottle", *changed_dependences
|
|
# Run postinstall on them because the tested formula might depend on
|
|
# this step
|
|
test "brew", "postinstall", *changed_dependences
|
|
end
|
|
end
|
|
test "brew", "fetch", "--retry", *fetch_args
|
|
test "brew", "uninstall", "--force", formula_name if formula.installed?
|
|
|
|
# shared_*_args are applied to both the main and --devel spec
|
|
shared_install_args = ["--verbose"]
|
|
shared_install_args << "--keep-tmp" if ARGV.keep_tmp?
|
|
# install_args is just for the main (stable, or devel if in a devel-only tap) spec
|
|
install_args = []
|
|
install_args << "--build-bottle" if !ARGV.include?("--fast") && !ARGV.include?("--no-bottle") && !formula.bottle_disabled?
|
|
install_args << "--HEAD" if ARGV.include? "--HEAD"
|
|
|
|
# Pass --devel or --HEAD to install in the event formulae lack stable. Supports devel-only/head-only.
|
|
# head-only should not have devel, but devel-only can have head. Stable can have all three.
|
|
if devel_only_tap? formula
|
|
install_args << "--devel"
|
|
formula_bottled = false
|
|
elsif head_only_tap? formula
|
|
install_args << "--HEAD"
|
|
formula_bottled = false
|
|
else
|
|
formula_bottled = formula.bottled?
|
|
end
|
|
|
|
install_args.concat(shared_install_args)
|
|
install_args << formula_name
|
|
# Don't care about e.g. bottle failures for dependencies.
|
|
install_passed = false
|
|
run_as_not_developer do
|
|
if !ARGV.include?("--fast") || formula_bottled || formula.bottle_unneeded?
|
|
test "brew", "install", "--only-dependencies", *install_args unless dependencies.empty?
|
|
test "brew", "install", *install_args
|
|
install_passed = steps.last.passed?
|
|
end
|
|
end
|
|
test "brew", "audit", *audit_args
|
|
if install_passed
|
|
if formula.stable? && !ARGV.include?("--fast") && !ARGV.include?("--no-bottle") && !formula.bottle_disabled?
|
|
bottle_args = ["--verbose", "--json", formula_name]
|
|
bottle_args << "--keep-old" if ARGV.include? "--keep-old"
|
|
bottle_args << "--skip-relocation" if ARGV.include? "--skip-relocation"
|
|
test "brew", "bottle", *bottle_args
|
|
bottle_step = steps.last
|
|
if bottle_step.passed? && bottle_step.has_output?
|
|
bottle_filename =
|
|
bottle_step.output.gsub(/.*(\.\/\S+#{Utils::Bottles::native_regex}).*/m, '\1')
|
|
bottle_json_filename = bottle_filename.gsub(/\.(\d+\.)?tar\.gz$/, ".json")
|
|
bottle_merge_args = ["--merge", "--write", "--no-commit", bottle_json_filename]
|
|
bottle_merge_args << "--keep-old" if ARGV.include? "--keep-old"
|
|
test "brew", "bottle", *bottle_merge_args
|
|
test "brew", "uninstall", "--force", formula_name
|
|
FileUtils.ln bottle_filename, HOMEBREW_CACHE/bottle_filename, :force => true
|
|
@formulae.delete(formula_name)
|
|
if unchanged_build_dependencies.any?
|
|
test "brew", "uninstall", "--force", *unchanged_build_dependencies
|
|
unchanged_dependencies -= unchanged_build_dependencies
|
|
end
|
|
test "brew", "install", bottle_filename
|
|
end
|
|
end
|
|
shared_test_args = ["--verbose"]
|
|
shared_test_args << "--keep-tmp" if ARGV.keep_tmp?
|
|
test "brew", "test", formula_name, *shared_test_args if formula.test_defined?
|
|
bottled_dependents.each do |dependent|
|
|
unless dependent.installed?
|
|
test "brew", "fetch", "--retry", dependent.name
|
|
next if steps.last.failed?
|
|
conflicts = dependent.conflicts.map { |c| Formulary.factory(c.name) }.select(&:installed?)
|
|
dependent.recursive_dependencies.each do |dependency|
|
|
conflicts += dependency.to_formula.conflicts.map { |c| Formulary.factory(c.name) }.select(&:installed?)
|
|
end
|
|
conflicts.each do |conflict|
|
|
test "brew", "unlink", conflict.name
|
|
end
|
|
unless ARGV.include?("--fast")
|
|
run_as_not_developer { test "brew", "install", dependent.name }
|
|
next if steps.last.failed?
|
|
end
|
|
end
|
|
if dependent.installed?
|
|
test "brew", "linkage", "--test", dependent.name
|
|
if testable_dependents.include? dependent
|
|
test "brew", "test", "--verbose", dependent.name
|
|
end
|
|
end
|
|
end
|
|
test "brew", "uninstall", "--force", formula_name
|
|
end
|
|
|
|
if formula.devel && formula.stable? \
|
|
&& !ARGV.include?("--HEAD") && !ARGV.include?("--fast") \
|
|
&& satisfied_requirements?(formula, :devel)
|
|
test "brew", "fetch", "--retry", "--devel", *fetch_args
|
|
run_as_not_developer do
|
|
test "brew", "install", "--devel", formula_name, *shared_install_args
|
|
end
|
|
devel_install_passed = steps.last.passed?
|
|
test "brew", "audit", "--devel", *audit_args
|
|
if devel_install_passed
|
|
test "brew", "test", "--devel", formula_name, *shared_test_args if formula.test_defined?
|
|
test "brew", "uninstall", "--devel", "--force", formula_name
|
|
end
|
|
end
|
|
test "brew", "uninstall", "--force", *unchanged_dependencies if unchanged_dependencies.any?
|
|
end
|
|
|
|
def homebrew
|
|
@category = __method__
|
|
return if @skip_homebrew
|
|
|
|
if @tap.nil?
|
|
tests_args = []
|
|
if RUBY_TWO
|
|
tests_args << "--official-cmd-taps"
|
|
tests_args << "--coverage" if ENV["TRAVIS"]
|
|
end
|
|
test "brew", "tests", *tests_args
|
|
test "brew", "tests", "--generic", "--only=integration_cmds",
|
|
*tests_args
|
|
test "brew", "tests", "--no-compat"
|
|
test "brew", "readall", "--syntax"
|
|
else
|
|
test "brew", "readall", "--aliases", @tap.name
|
|
end
|
|
end
|
|
|
|
def cleanup_before
|
|
@category = __method__
|
|
return unless ARGV.include? "--cleanup"
|
|
git "gc", "--auto"
|
|
git "stash"
|
|
git "am", "--abort"
|
|
git "rebase", "--abort"
|
|
git "reset", "--hard"
|
|
git "checkout", "-f", "master"
|
|
git "clean", "-ffdx"
|
|
HOMEBREW_REPOSITORY.cd do
|
|
safe_system "git", "reset", "--hard"
|
|
safe_system "git", "checkout", "-f", "master"
|
|
# This will uninstall all formulae, as long as
|
|
# HOMEBREW_REPOSITORY == HOMEBREW_PREFIX, which is true on the test bots
|
|
safe_system "git", "clean", "-ffdx", "--exclude=/Library/Taps/" unless ENV["HOMEBREW_RUBY"] == "1.8.7"
|
|
end
|
|
pr_locks = "#{@repository}/.git/refs/remotes/*/pr/*/*.lock"
|
|
Dir.glob(pr_locks) { |lock| FileUtils.rm_rf lock }
|
|
end
|
|
|
|
def cleanup_after
|
|
@category = __method__
|
|
|
|
if @start_branch && !@start_branch.empty? && \
|
|
(ARGV.include?("--cleanup") || @url || @hash)
|
|
checkout_args = [@start_branch]
|
|
checkout_args << "-f" if ARGV.include? "--cleanup"
|
|
test "git", "checkout", *checkout_args
|
|
end
|
|
|
|
if ARGV.include? "--cleanup"
|
|
test "git", "reset", "--hard"
|
|
git "stash", "pop"
|
|
test "brew", "cleanup", "--prune=7"
|
|
git "gc", "--auto"
|
|
test "git", "clean", "-ffdx"
|
|
HOMEBREW_REPOSITORY.cd do
|
|
safe_system "git", "reset", "--hard"
|
|
Tap.names.each { |s| safe_system "brew", "untap", s if s != "homebrew/core" }
|
|
safe_system "git", "clean", "-ffdx", "--exclude=/Library/Taps/"
|
|
end
|
|
if ARGV.include? "--local"
|
|
FileUtils.rm_rf ENV["HOMEBREW_HOME"]
|
|
FileUtils.rm_rf ENV["HOMEBREW_LOGS"]
|
|
end
|
|
end
|
|
|
|
FileUtils.rm_rf @brewbot_root unless ARGV.include? "--keep-logs"
|
|
end
|
|
|
|
def test(*args)
|
|
options = Hash === args.last ? args.pop : {}
|
|
options[:repository] = @repository
|
|
step = Step.new self, args, options
|
|
step.run
|
|
steps << step
|
|
step
|
|
end
|
|
|
|
def check_results
|
|
steps.all? do |step|
|
|
case step.status
|
|
when :passed then true
|
|
when :running then raise
|
|
when :failed then false
|
|
end
|
|
end
|
|
end
|
|
|
|
def formulae
|
|
changed_formulae_dependents = {}
|
|
|
|
@formulae.each do |formula|
|
|
formula_dependencies = Utils.popen_read("brew", "deps", "--include-build", formula).split("\n")
|
|
unchanged_dependencies = formula_dependencies - @formulae
|
|
changed_dependences = formula_dependencies - unchanged_dependencies
|
|
changed_dependences.each do |changed_formula|
|
|
changed_formulae_dependents[changed_formula] ||= 0
|
|
changed_formulae_dependents[changed_formula] += 1
|
|
end
|
|
end
|
|
|
|
changed_formulae = changed_formulae_dependents.sort do |a1, a2|
|
|
a2[1].to_i <=> a1[1].to_i
|
|
end
|
|
changed_formulae.map!(&:first)
|
|
unchanged_formulae = @formulae - changed_formulae
|
|
changed_formulae + unchanged_formulae
|
|
end
|
|
|
|
def head_only_tap?(formula)
|
|
formula.head && formula.devel.nil? && formula.stable.nil? && formula.tap == "homebrew/homebrew-head-only"
|
|
end
|
|
|
|
def devel_only_tap?(formula)
|
|
formula.devel && formula.stable.nil? && formula.tap == "homebrew/homebrew-devel-only"
|
|
end
|
|
|
|
def run
|
|
cleanup_before
|
|
begin
|
|
download
|
|
setup
|
|
homebrew
|
|
formulae.each do |f|
|
|
formula(f)
|
|
end
|
|
ensure
|
|
cleanup_after
|
|
end
|
|
check_results
|
|
end
|
|
end
|
|
|
|
def test_ci_upload(tap)
|
|
# Don't trust formulae we're uploading
|
|
ENV["HOMEBREW_DISABLE_LOAD_FORMULA"] = "1"
|
|
|
|
jenkins = ENV["JENKINS_HOME"]
|
|
job = ENV["UPSTREAM_JOB_NAME"]
|
|
id = ENV["UPSTREAM_BUILD_ID"]
|
|
raise "Missing Jenkins variables!" if !jenkins || !job || !id
|
|
|
|
bintray_user = ENV["BINTRAY_USER"]
|
|
bintray_key = ENV["BINTRAY_KEY"]
|
|
if !bintray_user || !bintray_key
|
|
raise "Missing BINTRAY_USER or BINTRAY_KEY variables!"
|
|
end
|
|
|
|
# Don't pass keys/cookies to subprocesses
|
|
ENV["BINTRAY_KEY"] = nil
|
|
ENV["HUDSON_SERVER_COOKIE"] = nil
|
|
ENV["JENKINS_SERVER_COOKIE"] = nil
|
|
ENV["HUDSON_COOKIE"] = nil
|
|
|
|
ARGV << "--verbose"
|
|
|
|
bottles = Dir["#{jenkins}/jobs/#{job}/configurations/axis-version/*/builds/#{id}/archive/*.bottle*.*"]
|
|
return if bottles.empty?
|
|
FileUtils.cp bottles, Dir.pwd, :verbose => true
|
|
|
|
json_files = Dir.glob("*.bottle.json")
|
|
bottles_hash = json_files.reduce({}) do |hash, json_file|
|
|
deep_merge_hashes hash, Utils::JSON.load(IO.read(json_file))
|
|
end
|
|
|
|
first_formula_name = bottles_hash.keys.first
|
|
tap = Tap.fetch(first_formula_name.rpartition("/").first.chuzzle || "homebrew/core")
|
|
|
|
ENV["GIT_AUTHOR_NAME"] = ENV["GIT_COMMITTER_NAME"] = "BrewTestBot"
|
|
ENV["GIT_AUTHOR_EMAIL"] = ENV["GIT_COMMITTER_EMAIL"] = "brew-test-bot@googlegroups.com"
|
|
ENV["GIT_WORK_TREE"] = tap.path
|
|
ENV["GIT_DIR"] = "#{ENV["GIT_WORK_TREE"]}/.git"
|
|
|
|
pr = ENV["UPSTREAM_PULL_REQUEST"]
|
|
number = ENV["UPSTREAM_BUILD_NUMBER"]
|
|
|
|
quiet_system "git", "am", "--abort"
|
|
quiet_system "git", "rebase", "--abort"
|
|
safe_system "git", "checkout", "-f", "master"
|
|
safe_system "git", "reset", "--hard", "origin/master"
|
|
safe_system "brew", "update"
|
|
|
|
if pr
|
|
pull_pr = "https://github.com/#{tap.user}/homebrew-#{tap.repo}/pull/#{pr}"
|
|
safe_system "brew", "pull", "--clean", pull_pr
|
|
end
|
|
|
|
system "brew", "bottle", "--merge", "--write", *json_files
|
|
|
|
remote = "git@github.com:BrewTestBot/homebrew-#{tap.repo}.git"
|
|
git_tag = pr ? "pr-#{pr}" : "testing-#{number}"
|
|
safe_system "git", "push", "--force", remote, "master:master", ":refs/tags/#{git_tag}"
|
|
|
|
formula_packaged = {}
|
|
|
|
bottles_hash.each do |formula_name, bottle_hash|
|
|
version = bottle_hash["formula"]["pkg_version"]
|
|
bintray_package = bottle_hash["bintray"]["package"]
|
|
bintray_repo = bottle_hash["bintray"]["repository"]
|
|
bintray_repo_url = "https://api.bintray.com/packages/homebrew/#{bintray_repo}"
|
|
|
|
bottle_hash["bottle"]["tags"].each do |tag, tag_hash|
|
|
filename = tag_hash["filename"]
|
|
if system "curl", "-I", "--silent", "--fail", "--output", "/dev/null",
|
|
"#{BottleSpecification::DEFAULT_DOMAIN}/#{bintray_repo}/#{filename}"
|
|
raise <<-EOS.undent
|
|
#{filename} is already published. Please remove it manually from
|
|
https://bintray.com/homebrew/#{bintray_repo}/#{bintray_package}/view#files
|
|
EOS
|
|
end
|
|
|
|
unless formula_packaged[formula_name]
|
|
package_url = "#{bintray_repo_url}/#{bintray_package}"
|
|
unless system "curl", "--silent", "--fail", "--output", "/dev/null", package_url
|
|
package_blob = <<-EOS.undent
|
|
{"name": "#{bintray_package}",
|
|
"public_download_numbers": true,
|
|
"public_stats": true}
|
|
EOS
|
|
curl "--silent", "--fail", "-u#{bintray_user}:#{bintray_key}",
|
|
"-H", "Content-Type: application/json",
|
|
"-d", package_blob, bintray_repo_url
|
|
puts
|
|
end
|
|
formula_packaged[formula_name] = true
|
|
end
|
|
|
|
content_url = "https://api.bintray.com/content/homebrew"
|
|
content_url += "/#{bintray_repo}/#{bintray_package}/#{version}/#{filename}"
|
|
content_url += "?override=1"
|
|
curl "--silent", "--fail", "-u#{bintray_user}:#{bintray_key}",
|
|
"-T", filename, content_url
|
|
puts
|
|
end
|
|
end
|
|
|
|
safe_system "git", "tag", "--force", git_tag
|
|
safe_system "git", "push", "--force", remote, "master:master", "refs/tags/#{git_tag}"
|
|
end
|
|
|
|
def sanitize_ARGV_and_ENV
|
|
if Pathname.pwd == HOMEBREW_PREFIX && ARGV.include?("--cleanup")
|
|
odie "cannot use --cleanup from HOMEBREW_PREFIX as it will delete all output."
|
|
end
|
|
|
|
ENV["HOMEBREW_DEVELOPER"] = "1"
|
|
ENV["HOMEBREW_SANDBOX"] = "1"
|
|
ENV["HOMEBREW_RUBY_MACHO"] = "1" if RUBY_TWO
|
|
ENV["HOMEBREW_NO_EMOJI"] = "1"
|
|
ENV["HOMEBREW_FAIL_LOG_LINES"] = "150"
|
|
ENV["HOMEBREW_EXPERIMENTAL_FILTER_FLAGS_ON_DEPS"] = "1"
|
|
|
|
if ENV["TRAVIS"]
|
|
ARGV << "--verbose"
|
|
ARGV << "--ci-master" if ENV["TRAVIS_PULL_REQUEST"] == "false"
|
|
ENV["HOMEBREW_VERBOSE_USING_DOTS"] = "1"
|
|
end
|
|
|
|
if ARGV.include?("--ci-master") || ARGV.include?("--ci-pr") \
|
|
|| ARGV.include?("--ci-testing")
|
|
ARGV << "--cleanup" if ENV["JENKINS_HOME"]
|
|
ARGV << "--junit" << "--local"
|
|
end
|
|
if ARGV.include? "--ci-master"
|
|
ARGV << "--fast"
|
|
end
|
|
|
|
if ARGV.include? "--local"
|
|
ENV["HOMEBREW_CACHE"] = "#{ENV["HOME"]}/Library/Caches/Homebrew"
|
|
mkdir_p ENV["HOMEBREW_CACHE"]
|
|
ENV["HOMEBREW_HOME"] = ENV["HOME"] = "#{Dir.pwd}/home"
|
|
mkdir_p ENV["HOME"]
|
|
ENV["HOMEBREW_LOGS"] = "#{Dir.pwd}/logs"
|
|
end
|
|
end
|
|
|
|
def test_bot
|
|
sanitize_ARGV_and_ENV
|
|
|
|
tap = resolve_test_tap
|
|
# Tap repository if required, this is done before everything else
|
|
# because Formula parsing and/or git commit hash lookup depends on it.
|
|
# At the same time, make sure Tap is not a shallow clone.
|
|
# bottle revision and bottle upload rely on full clone.
|
|
safe_system "brew", "tap", tap.name, "--full" if tap
|
|
|
|
if ARGV.include? "--ci-upload"
|
|
return test_ci_upload(tap)
|
|
end
|
|
|
|
tests = []
|
|
any_errors = false
|
|
skip_homebrew = ARGV.include?("--skip-homebrew")
|
|
if ARGV.named.empty?
|
|
# With no arguments just build the most recent commit.
|
|
head_test = Test.new("HEAD", :tap => tap, :skip_homebrew => skip_homebrew)
|
|
any_errors = !head_test.run
|
|
tests << head_test
|
|
else
|
|
ARGV.named.each do |argument|
|
|
test_error = false
|
|
begin
|
|
test = Test.new(argument, :tap => tap, :skip_homebrew => skip_homebrew)
|
|
skip_homebrew = true
|
|
rescue ArgumentError => e
|
|
test_error = true
|
|
ofail e.message
|
|
else
|
|
test_error = !test.run
|
|
tests << test
|
|
end
|
|
any_errors ||= test_error
|
|
end
|
|
end
|
|
|
|
if ARGV.include? "--junit"
|
|
xml_document = REXML::Document.new
|
|
xml_document << REXML::XMLDecl.new
|
|
testsuites = xml_document.add_element "testsuites"
|
|
|
|
tests.each do |test|
|
|
testsuite = testsuites.add_element "testsuite"
|
|
testsuite.add_attribute "name", "brew-test-bot.#{MacOS.cat}"
|
|
testsuite.add_attribute "tests", test.steps.count
|
|
|
|
test.steps.each do |step|
|
|
testcase = testsuite.add_element "testcase"
|
|
testcase.add_attribute "name", step.command_short
|
|
testcase.add_attribute "status", step.status
|
|
testcase.add_attribute "time", step.time
|
|
|
|
if step.has_output?
|
|
output = sanitize_output_for_xml(step.output)
|
|
cdata = REXML::CData.new output
|
|
|
|
if step.passed?
|
|
elem = testcase.add_element "system-out"
|
|
else
|
|
elem = testcase.add_element "failure"
|
|
elem.add_attribute "message", "#{step.status}: #{step.command.join(" ")}"
|
|
end
|
|
|
|
elem << cdata
|
|
end
|
|
end
|
|
end
|
|
|
|
open("brew-test-bot.xml", "w") do |xml_file|
|
|
pretty_print_indent = 2
|
|
xml_document.write(xml_file, pretty_print_indent)
|
|
end
|
|
end
|
|
ensure
|
|
if ARGV.include? "--clean-cache"
|
|
HOMEBREW_CACHE.children.each(&:rmtree)
|
|
else
|
|
Dir.glob("*.bottle*.tar.gz") do |bottle_file|
|
|
FileUtils.rm_f HOMEBREW_CACHE/bottle_file
|
|
end
|
|
end
|
|
|
|
Homebrew.failed = any_errors
|
|
end
|
|
|
|
def sanitize_output_for_xml(output)
|
|
unless output.empty?
|
|
# Remove invalid XML CData characters from step output.
|
|
if ruby_has_encoding?
|
|
# This is the regex for valid XML chars, but only works in Ruby 2.0+
|
|
# /[\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/
|
|
# For 1.9 compatibility, use the inverse of that, which stays under \u10000
|
|
# invalid_xml_pat = /[\x00-\x08\x0B\x0C\x0E-\x1F\uD800-\uDFFF\uFFFE\uFFFF]/
|
|
# But Ruby won't allow you to reference surrogates, so we have:
|
|
invalid_xml_pat = /[\x00-\x08\x0B\x0C\x0E-\x1F\uFFFE\uFFFF]/
|
|
output = output.gsub(invalid_xml_pat, "\uFFFD")
|
|
else
|
|
# Invalid XML chars, as far as single-byte chars go
|
|
output = output.delete("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f" \
|
|
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f")
|
|
end
|
|
|
|
# Truncate to 1MB to avoid hitting CI limits
|
|
if output.bytesize > MAX_STEP_OUTPUT_SIZE
|
|
output = truncate_text_to_approximate_size(output, MAX_STEP_OUTPUT_SIZE, :front_weight => 0.0)
|
|
output = "truncated output to 1MB:\n" + output
|
|
end
|
|
end
|
|
output
|
|
end
|
|
end
|