aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/dev-cmd
diff options
context:
space:
mode:
authorMike McQuaid2016-05-30 11:17:52 +0100
committerMike McQuaid2016-05-30 11:17:52 +0100
commitd20c1ed1465f55f021be44beef737c18d9769dd8 (patch)
tree9adda84acad4de087d2fc05afe417e4c6f4e1103 /Library/Homebrew/dev-cmd
parent055147153654b99798a7adb55b4128344919c3e2 (diff)
downloadbrew-d20c1ed1465f55f021be44beef737c18d9769dd8.tar.bz2
test-bot: move to dev-cmd. (#298)
This is not a command designed for end-users.
Diffstat (limited to 'Library/Homebrew/dev-cmd')
-rw-r--r--Library/Homebrew/dev-cmd/test-bot.rb1023
1 files changed, 1023 insertions, 0 deletions
diff --git a/Library/Homebrew/dev-cmd/test-bot.rb b/Library/Homebrew/dev-cmd/test-bot.rb
new file mode 100644
index 000000000..3b1e410e0
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/test-bot.rb
@@ -0,0 +1,1023 @@
+# 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.
+# --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.
+#
+# --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.
+
+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-]+)$}
+
+ def ruby_has_encoding?
+ String.method_defined?(:force_encoding)
+ end
+
+ 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
+
+ class Step
+ attr_reader :command, :name, :status, :output, :time
+
+ 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
+ @time = 0
+ 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
+
+ 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 = Homebrew.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 file.extname == ".rb"
+ @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"
+ # 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.
+ unless travis_pr
+ 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"
+ test "brew", "doctor" if !ENV["TRAVIS"] && ENV["HOMEBREW_RUBY"] != "1.8.7"
+ 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"
+ 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?)
+ 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
+
+ ruby_two = RUBY_VERSION.split(".").first.to_i >= 2
+
+ if @tap.nil?
+ tests_args = []
+ tests_args << "--coverage" if ruby_two && ENV["TRAVIS"]
+ test "brew", "tests", *tests_args
+ test "brew", "tests", "--no-compat"
+ elsif @tap.core_tap?
+ readall_args = []
+ readall_args << "--syntax" if ruby_two
+ test "brew", "readall", "--aliases", *readall_args
+ else
+ test "brew", "readall", @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)
+ raise "Need a tap to upload!" unless 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
+
+ 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
+
+ json_files = Dir.glob("*.bottle.json")
+ 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 = json_files.reduce({}) do |hash, json_file|
+ deep_merge_hashes hash, Utils::JSON.load(IO.read(json_file))
+ end
+
+ 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_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_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
+ output = output.delete("\000\a\b\e\f\x2\x1f")
+ end
+
+ # Truncate to 1MB to avoid hitting CI limits
+ if output.bytesize > MAX_STEP_OUTPUT_SIZE
+ if ruby_has_encoding?
+ binary_output = output.force_encoding("BINARY")
+ output = binary_output.slice(-MAX_STEP_OUTPUT_SIZE, MAX_STEP_OUTPUT_SIZE)
+ fix_encoding!(output)
+ else
+ output = output.slice(-MAX_STEP_OUTPUT_SIZE, MAX_STEP_OUTPUT_SIZE)
+ end
+ output = "truncated output to 1MB:\n" + output
+ end
+ end
+ output
+ end
+end
+