aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Contributions/cmd/brew-test-bot.rb
diff options
context:
space:
mode:
Diffstat (limited to 'Library/Contributions/cmd/brew-test-bot.rb')
-rwxr-xr-xLibrary/Contributions/cmd/brew-test-bot.rb296
1 files changed, 296 insertions, 0 deletions
diff --git a/Library/Contributions/cmd/brew-test-bot.rb b/Library/Contributions/cmd/brew-test-bot.rb
new file mode 100755
index 000000000..58e4f5e6c
--- /dev/null
+++ b/Library/Contributions/cmd/brew-test-bot.rb
@@ -0,0 +1,296 @@
+# 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.
+# --skip-setup: Don't check the local system is setup correctly.
+
+require 'formula'
+require 'utils'
+require 'date'
+
+HOMEBREW_CONTRIBUTED_CMDS = HOMEBREW_REPOSITORY + "Library/Contributions/cmd/"
+
+class Step
+ attr_reader :command, :repository
+ attr_accessor :status
+
+ def initialize test, command
+ @test = test
+ @category = test.category
+ @command = command
+ @name = command.split[1].delete '-'
+ @status = :running
+ @repository = HOMEBREW_REPOSITORY
+ @test.steps << self
+ end
+
+ def log_file_path full_path=true
+ file = "#{@category}.#{@name}.txt"
+ return file unless @test.log_root and full_path
+ @test.log_root + 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 puts_command
+ print "#{Tty.blue}==>#{Tty.white} #{@command}#{Tty.reset}"
+ tabs = (80 - "PASSED".length + 1 - @command.length) / 8
+ tabs.times{ print "\t" }
+ $stdout.flush
+ end
+
+ def puts_result
+ puts "#{Tty.send status_colour}#{status_upcase}#{Tty.reset}"
+ end
+
+ def self.run test, command, puts_output_on_success = false
+ step = new test, command
+ step.puts_command
+
+ command = "#{step.command} &>#{step.log_file_path}"
+
+ output = nil
+ if command.start_with? 'git '
+ Dir.chdir step.repository do
+ output = `#{command}`
+ end
+ else
+ output = `#{command}`
+ end
+ output = IO.read(step.log_file_path)
+
+ success = $?.success?
+ step.status = success ? :passed : :failed
+ step.puts_result
+ if output and output.any? and (not success or puts_output_on_success)
+ puts output
+ end
+ FileUtils.rm step.log_file_path unless ARGV.include? "--keep-logs"
+ end
+end
+
+class Test
+ attr_reader :log_root, :category, :name
+ attr_reader :core_changed, :formulae
+ attr_accessor :steps
+
+ def initialize argument
+ @hash = nil
+ @url = nil
+ @formulae = []
+
+ url_match = argument.match HOMEBREW_PULL_URL_REGEX
+ formula = Formula.factory argument rescue FormulaUnavailableError
+ git "rev-parse --verify #{argument} &>/dev/null"
+ 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__
+ @steps = []
+ @core_changed = false
+ @brewbot_root = Pathname.pwd + "brewbot"
+ FileUtils.mkdir_p @brewbot_root
+ end
+
+ def git arguments
+ Dir.chdir HOMEBREW_REPOSITORY do
+ `git #{arguments}`
+ end
+ end
+
+ def download
+ def current_sha1
+ git('rev-parse --short HEAD').strip
+ end
+
+ def current_branch
+ git('symbolic-ref HEAD').gsub('refs/heads/', '').strip
+ end
+
+ @category = __method__
+ @start_branch = current_branch
+
+ if @hash or @url
+ diff_start_sha1 = current_sha1
+ test "brew update" if current_branch == "master"
+ diff_end_sha1 = current_sha1
+ end
+
+ if @hash == 'HEAD'
+ @name = "#{diff_start_sha1}-#{diff_end_sha1}"
+ 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
+ @name = "#{@url}-#{diff_end_sha1}"
+ 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 steps.last.status != :passed
+
+ diff_stat = git "diff #{diff_start_sha1}..#{diff_end_sha1} --name-status"
+ diff_stat.each_line do |line|
+ status, filename = line.split
+ # Don't try and do anything to removed files.
+ if (status == 'A' or status == 'M')
+ if filename.include? '/Formula/'
+ @formulae << File.basename(filename, '.rb')
+ end
+ end
+ if filename.include? '/Homebrew/' or filename.include? '/ENV/' \
+ or filename.include? 'bin/brew'
+ @core_changed = true
+ end
+ end
+ end
+
+ def setup
+ @category = __method__
+
+ test "brew doctor"
+ test "brew --env"
+ test "brew --config"
+ end
+
+ def formula formula
+ @category = __method__.to_s + ".#{formula}"
+
+ dependencies = `brew deps #{formula}`.split("\n")
+ dependencies -= `brew list`.split("\n")
+ dependencies = dependencies.join(' ')
+ formula_object = Formula.factory(formula)
+
+ test "brew audit #{formula}"
+ test "brew fetch #{dependencies}" unless dependencies.empty?
+ test "brew fetch --build-bottle #{formula}"
+ test "brew install --verbose #{dependencies}" unless dependencies.empty?
+ test "brew install --verbose --build-bottle #{formula}"
+ return unless steps.last.status == :passed
+ test "brew bottle #{formula}", true
+ bottle_version = bottle_new_version(formula_object)
+ bottle_filename = bottle_filename(formula_object, bottle_version)
+ test "brew uninstall #{formula}"
+ test "brew install #{bottle_filename}"
+ test "brew test #{formula}" if formula_object.test_defined?
+ test "brew uninstall #{formula}"
+ test "brew uninstall #{dependencies}" unless 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 2>/dev/null'
+ git 'rebase --abort 2>/dev/null'
+ git 'reset --hard'
+ git 'clean --force -dx'
+ end
+
+ def cleanup_after
+ @category = __method__
+ force_flag = ''
+ if ARGV.include? '--cleanup'
+ test 'brew cleanup'
+ test 'git clean --force -dx'
+ force_flag = '-f'
+ end
+
+ if ARGV.include? '--cleanup' or @url or @hash
+ test "git checkout #{force_flag} #{@start_branch}"
+ end
+
+ if ARGV.include? '--cleanup'
+ test 'git reset --hard'
+ test 'git gc'
+ git 'stash pop 2>/dev/null'
+ end
+
+ FileUtils.rm_rf @brewbot_root unless ARGV.include? "--keep-logs"
+ end
+
+ def test cmd, puts_output_on_success = false
+ Step.run self, cmd, puts_output_on_success
+ end
+
+ def check_results
+ message = "All tests passed and raring to brew."
+
+ status = :passed
+ steps.each do |step|
+ case step.status
+ when :passed then next
+ when :running then raise
+ when :failed then
+ if status == :passed
+ status = :failed
+ message = ""
+ end
+ message += "#{step.command}: #{step.status.to_s.upcase}\n"
+ end
+ end
+ status == :passed
+ end
+
+ def self.run argument
+ test = new argument
+ test.cleanup_before
+ test.download
+ test.setup unless ARGV.include? "--skip-setup"
+ test.formulae.each do |formula|
+ test.formula formula
+ end
+ test.homebrew if test.core_changed
+ test.cleanup_after
+ test.check_results
+ end
+end
+
+if Pathname.pwd == HOMEBREW_PREFIX and ARGV.include? "--cleanup"
+ odie 'cannot use --cleanup from HOMEBREW_PREFIX as it will delete all output.'
+end
+
+any_errors = false
+if ARGV.named.empty?
+ # With no arguments just build the most recent commit.
+ any_errors = Test.run 'HEAD'
+else
+ ARGV.named.each { |argument| any_errors = Test.run(argument) or any_errors }
+end
+exit any_errors ? 0 : 1