From d20c1ed1465f55f021be44beef737c18d9769dd8 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Mon, 30 May 2016 11:17:52 +0100 Subject: test-bot: move to dev-cmd. (#298) This is not a command designed for end-users.--- Library/Homebrew/cmd/test-bot.rb | 1023 ---------------------------------- Library/Homebrew/dev-cmd/test-bot.rb | 1023 ++++++++++++++++++++++++++++++++++ 2 files changed, 1023 insertions(+), 1023 deletions(-) delete mode 100644 Library/Homebrew/cmd/test-bot.rb create mode 100644 Library/Homebrew/dev-cmd/test-bot.rb (limited to 'Library') diff --git a/Library/Homebrew/cmd/test-bot.rb b/Library/Homebrew/cmd/test-bot.rb deleted file mode 100644 index 3b1e410e0..000000000 --- a/Library/Homebrew/cmd/test-bot.rb +++ /dev/null @@ -1,1023 +0,0 @@ -# Comprehensively test a formula or pull request. -# -# Usage: brew test-bot [options...] -# -# 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=: 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 - 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...] +# +# 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=: 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 + -- cgit v1.2.3