aboutsummaryrefslogtreecommitdiffstats
path: root/Library
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
parentbd8559c791e7bc37c17a0f270e9b0f98e89d7a8a (diff)
downloadbrew-b7c9025d931a4e7894f8c3febf109031e775d528.tar.bz2
brew-test-bot: make an internal command.
Diffstat (limited to 'Library')
-rwxr-xr-xLibrary/Contributions/cmd/brew-test-bot.rb663
-rwxr-xr-xLibrary/Homebrew/cmd/test-bot.rb666
2 files changed, 666 insertions, 663 deletions
diff --git a/Library/Contributions/cmd/brew-test-bot.rb b/Library/Contributions/cmd/brew-test-bot.rb
deleted file mode 100755
index 771253173..000000000
--- a/Library/Contributions/cmd/brew-test-bot.rb
+++ /dev/null
@@ -1,663 +0,0 @@
-# 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'
-
-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_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
-
-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*.*"] + ["."]
- exit unless system "cp", *cp_args
-
- ENV["GIT_COMMITTER_NAME"] = "BrewTestBot"
- ENV["GIT_COMMITTER_EMAIL"] = "brew-test-bot@googlegroups.com"
- ENV["GIT_WORK_TREE"] = 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}"
- exit
-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"
-
-exit any_errors ? 0 : 1
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