aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cmd
diff options
context:
space:
mode:
authorMike McQuaid2014-09-20 14:24:52 +0100
committerMike McQuaid2014-09-24 15:22:37 -0700
commitb7c9025d931a4e7894f8c3febf109031e775d528 (patch)
tree4ef54f19f8b1e8d13b08df2e450d0e67d46dea2b /Library/Homebrew/cmd
parentbd8559c791e7bc37c17a0f270e9b0f98e89d7a8a (diff)
downloadbrew-b7c9025d931a4e7894f8c3febf109031e775d528.tar.bz2
brew-test-bot: make an internal command.
Diffstat (limited to 'Library/Homebrew/cmd')
-rwxr-xr-xLibrary/Homebrew/cmd/test-bot.rb666
1 files changed, 666 insertions, 0 deletions
diff --git a/Library/Homebrew/cmd/test-bot.rb b/Library/Homebrew/cmd/test-bot.rb
new file mode 100755
index 000000000..95bbc4686
--- /dev/null
+++ b/Library/Homebrew/cmd/test-bot.rb
@@ -0,0 +1,666 @@
+# 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.
+# --junit: Generate a JUnit XML test results file.
+# --email: Generate an email subject file.
+# --no-bottle: Run brew install without --build-bottle
+# --HEAD: Run brew install with --HEAD
+# --local: Ask Homebrew to write verbose logs under ./logs/
+# --tap=<tap>: Use the git repository of the given tap
+# --dry-run: Just print commands, don't run them.
+#
+# --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-pr-upload: Homebrew CI pull request bottle upload.
+# --ci-testing-upload: Homebrew CI testing bottle upload.
+
+require 'formula'
+require 'utils'
+require 'date'
+require 'rexml/document'
+require 'rexml/xmldecl'
+require 'rexml/cdata'
+
+module Homebrew
+ EMAIL_SUBJECT_FILE = "brew-test-bot.#{MacOS.cat}.email.txt"
+
+ def homebrew_git_repo tap=nil
+ if tap
+ HOMEBREW_LIBRARY/"Taps/#{tap}"
+ else
+ HOMEBREW_REPOSITORY
+ 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 status_colour
+ case @status
+ when :passed then "green"
+ when :running then "orange"
+ when :failed then "red"
+ end
+ end
+
+ def status_upcase
+ @status.to_s.upcase
+ end
+
+ def command_short
+ (@command - %w[brew --force --retry --verbose --build-bottle --rb]).join(" ")
+ end
+
+ def passed?
+ @status == :passed
+ end
+
+ def failed?
+ @status == :failed
+ end
+
+ def puts_command
+ cmd = @command.join(" ")
+ print "#{Tty.blue}==>#{Tty.white} #{cmd}#{Tty.reset}"
+ tabs = (80 - "PASSED".length + 1 - cmd.length) / 8
+ tabs.times{ print "\t" }
+ $stdout.flush
+ end
+
+ def puts_result
+ puts " #{Tty.send status_colour}#{status_upcase}#{Tty.reset}"
+ end
+
+ def has_output?
+ @output && !@output.empty?
+ end
+
+ def run
+ puts_command
+ if ARGV.include? "--dry-run"
+ puts
+ @status = :passed
+ return
+ end
+
+ start_time = Time.now
+
+ log = log_file_path
+
+ pid = fork do
+ File.open(log, "wb") do |f|
+ STDOUT.reopen(f)
+ STDERR.reopen(f)
+ end
+ Dir.chdir(@repository) if @command.first == "git"
+ exec(*@command)
+ end
+ Process.wait(pid)
+
+ @time = Time.now - start_time
+
+ @status = $?.success? ? :passed : :failed
+ puts_result
+
+ if File.exist?(log)
+ @output = File.read(log)
+ if has_output? and (failed? or @puts_output_on_success)
+ puts @output
+ end
+ FileUtils.rm(log) unless ARGV.include? "--keep-logs"
+ end
+ end
+ end
+
+ class Test
+ attr_reader :log_root, :category, :name, :steps
+
+ def initialize argument, tap=nil
+ @hash = nil
+ @url = nil
+ @formulae = []
+ @steps = []
+ @tap = tap
+ @repository = Homebrew.homebrew_git_repo @tap
+ @repository_requires_tapping = !@repository.directory?
+
+ url_match = argument.match HOMEBREW_PULL_OR_COMMIT_URL_REGEX
+
+ # Tap repository if required, this is done before everything else
+ # because Formula parsing and/or git commit hash lookup depends on it.
+ test "brew", "tap", @tap if @tap && @repository_requires_tapping
+
+ begin
+ formula = Formulary.factory(argument)
+ rescue FormulaUnavailableError
+ end
+
+ git "rev-parse", "--verify", "-q", argument
+ if $?.success?
+ @hash = argument
+ elsif url_match
+ @url = url_match[0]
+ elsif formula
+ @formulae = [argument]
+ else
+ odie "#{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 git(*args)
+ rd, wr = IO.pipe
+
+ pid = fork do
+ rd.close
+ STDERR.reopen("/dev/null")
+ STDOUT.reopen(wr)
+ wr.close
+ Dir.chdir @repository
+ exec("git", *args)
+ end
+ wr.close
+ Process.wait(pid)
+
+ rd.read
+ ensure
+ rd.close
+ 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
+
+ @category = __method__
+ @start_branch = current_branch
+
+ # Use Jenkins environment variables if present.
+ if no_args? and ENV['GIT_PREVIOUS_COMMIT'] and ENV['GIT_COMMIT'] \
+ and not ENV['ghprbPullId']
+ diff_start_sha1 = shorten_revision ENV['GIT_PREVIOUS_COMMIT']
+ diff_end_sha1 = shorten_revision ENV['GIT_COMMIT']
+ test "brew", "update" if current_branch == "master"
+ elsif @hash or @url
+ diff_start_sha1 = current_sha1
+ test "brew", "update" if current_branch == "master"
+ diff_end_sha1 = current_sha1
+ end
+
+ # Handle Jenkins pull request builder plugin.
+ if ENV['ghprbPullId'] and ENV['GIT_URL']
+ git_url = ENV['GIT_URL']
+ git_match = git_url.match %r{.*github.com[:/](\w+/\w+).*}
+ if git_match
+ github_repo = git_match[1]
+ pull_id = ENV['ghprbPullId']
+ @url = "https://github.com/#{github_repo}/pull/#{pull_id}"
+ @hash = nil
+ else
+ puts "Invalid 'ghprbPullId' environment variable value!"
+ end
+ end
+
+ if no_args?
+ if diff_start_sha1 == diff_end_sha1 or \
+ single_commit?(diff_start_sha1, diff_end_sha1)
+ @name = diff_end_sha1
+ else
+ @name = "#{diff_start_sha1}-#{diff_end_sha1}"
+ end
+ elsif @hash
+ test "git", "checkout", @hash
+ diff_start_sha1 = "#{@hash}^"
+ diff_end_sha1 = @hash
+ @name = @hash
+ elsif @url
+ test "git", "checkout", current_sha1
+ test "brew", "pull", "--clean", @url
+ diff_end_sha1 = current_sha1
+ @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
+ diff_start_sha1 = diff_end_sha1 = current_sha1
+ @name = "#{@formulae.first}-#{diff_end_sha1}"
+ end
+
+ @log_root = @brewbot_root + @name
+ FileUtils.mkdir_p @log_root
+
+ return unless diff_start_sha1 != diff_end_sha1
+ return if @url and not steps.last.passed?
+
+ if @tap
+ formula_path = %w[Formula HomebrewFormula].find { |dir| (@repository/dir).directory? } || ""
+ else
+ formula_path = "Library/Formula"
+ end
+
+ git(
+ "diff-tree", "-r", "--name-only", "--diff-filter=AM",
+ diff_start_sha1, diff_end_sha1, "--", formula_path
+ ).each_line do |line|
+ @formulae << File.basename(line.chomp, ".rb")
+ end
+ end
+
+ def skip formula
+ puts "#{Tty.blue}==>#{Tty.white} SKIPPING: #{formula}#{Tty.reset}"
+ end
+
+ def satisfied_requirements? formula_object, spec
+ requirements = formula_object.send(spec).requirements
+
+ unsatisfied_requirements = requirements.reject do |requirement|
+ requirement.satisfied? || requirement.default_formula?
+ end
+
+ if unsatisfied_requirements.empty?
+ true
+ else
+ formula = formula_object.name
+ formula += " (#{spec})" unless spec == :stable
+ skip formula
+ unsatisfied_requirements.each {|r| puts r.message}
+ false
+ end
+ end
+
+ def setup
+ @category = __method__
+ return if ARGV.include? "--skip-setup"
+ test "brew", "doctor"
+ test "brew", "--env"
+ test "brew", "config"
+ end
+
+ def formula formula
+ @category = __method__.to_s + ".#{formula}"
+
+ test "brew", "uses", formula
+ dependencies = `brew deps #{formula}`.split("\n")
+ dependencies -= `brew list`.split("\n")
+ unchanged_dependencies = dependencies - @formulae
+ changed_dependences = dependencies - unchanged_dependencies
+ formula_object = Formulary.factory(formula)
+ return unless satisfied_requirements?(formula_object, :stable)
+
+ installed_gcc = false
+ deps = formula_object.stable.deps.to_a
+ reqs = formula_object.stable.requirements.to_a
+ if formula_object.devel && !ARGV.include?('--HEAD')
+ deps |= formula_object.devel.deps.to_a
+ reqs |= formula_object.devel.requirements.to_a
+ end
+
+ begin
+ deps.each { |d| CompilerSelector.select_for(d.to_formula) }
+ CompilerSelector.select_for(formula_object)
+ rescue CompilerSelectionError => e
+ unless installed_gcc
+ test "brew", "install", "gcc"
+ installed_gcc = true
+ OS::Mac.clear_version_cache
+ retry
+ end
+ skip formula
+ puts e.message
+ return
+ end
+
+ if (deps | reqs).any? { |d| d.name == "mercurial" && d.build? }
+ test "brew", "install", "mercurial"
+ end
+
+ test "brew", "fetch", "--retry", *unchanged_dependencies unless unchanged_dependencies.empty?
+ test "brew", "fetch", "--retry", "--build-bottle", *changed_dependences unless changed_dependences.empty?
+ formula_fetch_options = []
+ formula_fetch_options << "--build-bottle" unless ARGV.include? "--no-bottle"
+ formula_fetch_options << "--force" if ARGV.include? "--cleanup"
+ formula_fetch_options << formula
+ test "brew", "fetch", "--retry", *formula_fetch_options
+ test "brew", "uninstall", "--force", formula if formula_object.installed?
+ install_args = %w[--verbose]
+ install_args << "--build-bottle" unless ARGV.include? "--no-bottle"
+ install_args << "--HEAD" if ARGV.include? "--HEAD"
+ install_args << formula
+ # Don't care about e.g. bottle failures for dependencies.
+ ENV["HOMEBREW_DEVELOPER"] = nil
+ test "brew", "install", "--only-dependencies", *install_args unless dependencies.empty?
+ ENV["HOMEBREW_DEVELOPER"] = "1"
+ test "brew", "install", *install_args
+ install_passed = steps.last.passed?
+ test "brew", "audit", formula
+ if install_passed
+ unless ARGV.include? '--no-bottle'
+ test "brew", "bottle", "--rb", formula, :puts_output_on_success => true
+ bottle_step = steps.last
+ if bottle_step.passed? and bottle_step.has_output?
+ bottle_filename =
+ bottle_step.output.gsub(/.*(\.\/\S+#{bottle_native_regex}).*/m, '\1')
+ test "brew", "uninstall", "--force", formula
+ test "brew", "install", bottle_filename
+ end
+ end
+ test "brew", "test", "--verbose", formula if formula_object.test_defined?
+ test "brew", "uninstall", "--force", formula
+ end
+
+ if formula_object.devel && !ARGV.include?('--HEAD') \
+ && satisfied_requirements?(formula_object, :devel)
+ test "brew", "fetch", "--retry", "--devel", *formula_fetch_options
+ test "brew", "install", "--devel", "--verbose", formula
+ devel_install_passed = steps.last.passed?
+ test "brew", "audit", "--devel", formula
+ if devel_install_passed
+ test "brew", "test", "--devel", "--verbose", formula if formula_object.test_defined?
+ test "brew", "uninstall", "--devel", "--force", formula
+ end
+ end
+ test "brew", "uninstall", "--force", *unchanged_dependencies unless unchanged_dependencies.empty?
+ end
+
+ def homebrew
+ @category = __method__
+ test "brew", "tests"
+ test "brew", "readall"
+ end
+
+ def cleanup_before
+ @category = __method__
+ return unless ARGV.include? '--cleanup'
+ git "stash"
+ git "am", "--abort"
+ git "rebase", "--abort"
+ git "reset", "--hard"
+ git "checkout", "-f", "master"
+ git "clean", "--force", "-dx"
+ end
+
+ def cleanup_after
+ @category = __method__
+
+ checkout_args = []
+ if ARGV.include? '--cleanup'
+ test "git", "clean", "--force", "-dx"
+ checkout_args << "-f"
+ end
+
+ checkout_args << @start_branch
+
+ if ARGV.include? '--cleanup' or @url or @hash
+ test "git", "checkout", *checkout_args
+ end
+
+ if ARGV.include? '--cleanup'
+ test "git", "reset", "--hard"
+ git "stash", "pop"
+ test "brew", "cleanup"
+ end
+
+ test "brew", "untap", @tap if @tap && @repository_requires_tapping
+
+ 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
+ status = :passed
+ steps.each do |step|
+ case step.status
+ when :passed then next
+ when :running then raise
+ when :failed then status = :failed
+ end
+ end
+ status == :passed
+ end
+
+ def formulae
+ changed_formulae_dependents = {}
+ dependencies = []
+ non_dependencies = []
+
+ @formulae.each do |formula|
+ formula_dependencies = `brew deps #{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 run
+ cleanup_before
+ download
+ setup
+ homebrew
+ formulae.each do |f|
+ formula(f)
+ end
+ cleanup_after
+ check_results
+ end
+ end
+
+ def test_bot
+ tap = ARGV.value('tap')
+
+ if Pathname.pwd == HOMEBREW_PREFIX and ARGV.include? "--cleanup"
+ odie 'cannot use --cleanup from HOMEBREW_PREFIX as it will delete all output.'
+ end
+
+ if ARGV.include? "--email"
+ File.open EMAIL_SUBJECT_FILE, 'w' do |file|
+ # The file should be written at the end but in case we don't get to that
+ # point ensure that we have something valid.
+ file.write "#{MacOS.version}: internal error."
+ end
+ end
+
+ ENV['HOMEBREW_DEVELOPER'] = '1'
+ ENV['HOMEBREW_NO_EMOJI'] = '1'
+ if ARGV.include? '--ci-master' or ARGV.include? '--ci-pr' \
+ or ARGV.include? '--ci-testing'
+ ARGV << '--cleanup' << '--junit' << '--local'
+ end
+ if ARGV.include? '--ci-master'
+ ARGV << '--no-bottle' << '--email'
+ end
+
+ if ARGV.include? '--local'
+ ENV['HOMEBREW_LOGS'] = "#{Dir.pwd}/logs"
+ end
+
+ if ARGV.include? '--ci-pr-upload' or ARGV.include? '--ci-testing-upload'
+ jenkins = ENV['JENKINS_HOME']
+ job = ENV['UPSTREAM_JOB_NAME']
+ id = ENV['UPSTREAM_BUILD_ID']
+ raise "Missing Jenkins variables!" unless jenkins and job and id
+
+ ARGV << '--verbose'
+ cp_args = Dir["#{jenkins}/jobs/#{job}/configurations/axis-version/*/builds/#{id}/archive/*.bottle*.*"] + ["."]
+ return unless system "cp", *cp_args
+
+ ENV["GIT_COMMITTER_NAME"] = "BrewTestBot"
+ ENV["GIT_COMMITTER_EMAIL"] = "brew-test-bot@googlegroups.com"
+ ENV["GIT_WORK_TREE"] = Homebrew.homebrew_git_repo tap
+ ENV["GIT_DIR"] = "#{ENV["GIT_WORK_TREE"]}/.git"
+
+ pr = ENV['UPSTREAM_PULL_REQUEST']
+ number = ENV['UPSTREAM_BUILD_NUMBER']
+
+ system "git am --abort 2>/dev/null"
+ system "git rebase --abort 2>/dev/null"
+ safe_system "git", "checkout", "-f", "master"
+ safe_system "git", "reset", "--hard", "origin/master"
+ safe_system "brew", "update"
+
+ if ARGV.include? '--ci-pr-upload'
+ safe_system "brew", "pull", "--clean", pr
+ end
+
+ ENV["GIT_AUTHOR_NAME"] = ENV["GIT_COMMITTER_NAME"]
+ ENV["GIT_AUTHOR_EMAIL"] = ENV["GIT_COMMITTER_EMAIL"]
+ safe_system "brew", "bottle", "--merge", "--write", *Dir["*.bottle.rb"]
+
+ remote = "git@github.com:BrewTestBot/homebrew.git"
+ tag = pr ? "pr-#{pr}" : "testing-#{number}"
+ safe_system "git", "push", "--force", remote, "master:master", ":refs/tags/#{tag}"
+
+ path = "/home/frs/project/m/ma/machomebrew/Bottles/"
+ url = "BrewTestBot,machomebrew@frs.sourceforge.net:#{path}"
+
+ rsync_args = %w[--partial --progress --human-readable --compress]
+ rsync_args += Dir["*.bottle*.tar.gz"] + [url]
+
+ safe_system "rsync", *rsync_args
+ safe_system "git", "tag", "--force", tag
+ safe_system "git", "push", "--force", remote, "refs/tags/#{tag}"
+ return
+ end
+
+ tests = []
+ any_errors = false
+ if ARGV.named.empty?
+ # With no arguments just build the most recent commit.
+ test = Test.new('HEAD', tap)
+ any_errors = test.run
+ tests << test
+ else
+ ARGV.named.each do |argument|
+ test = Test.new(argument, tap)
+ any_errors = test.run or any_errors
+ tests << test
+ 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.attributes['name'] = "brew-test-bot.#{MacOS.cat}"
+ testsuite.attributes['tests'] = test.steps.count
+ test.steps.each do |step|
+ testcase = testsuite.add_element 'testcase'
+ testcase.attributes['name'] = step.command_short
+ testcase.attributes['status'] = step.status
+ testcase.attributes['time'] = step.time
+ failure = testcase.add_element 'failure' if step.failed?
+ if step.has_output?
+ # Remove invalid XML CData characters from step output.
+ output = step.output
+ if output.respond_to?(:force_encoding) && !output.valid_encoding?
+ output.force_encoding(Encoding::UTF_8)
+ end
+ output = REXML::CData.new output.delete("\000\a\b\e\f")
+ if step.passed?
+ system_out = testcase.add_element 'system-out'
+ system_out.text = output
+ else
+ failure.attributes["message"] = "#{step.status}: #{step.command.join(" ")}"
+ failure.text = output
+ end
+ 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
+
+ if ARGV.include? "--email"
+ failed_steps = []
+ tests.each do |test|
+ test.steps.each do |step|
+ next unless step.failed?
+ failed_steps << step.command_short
+ end
+ end
+
+ if failed_steps.empty?
+ email_subject = ''
+ else
+ email_subject = "#{MacOS.version}: #{failed_steps.join ', '}."
+ end
+
+ File.open EMAIL_SUBJECT_FILE, 'w' do |file|
+ file.write email_subject
+ end
+ end
+
+ safe_system "rm -rf #{HOMEBREW_CACHE}/*" if ARGV.include? "--clean-cache"
+
+ Homebrew.failed = any_errors
+ end
+end