diff --git a/Library/Contributions/cmds/brew-test-bot.commit.html.erb b/Library/Contributions/cmds/brew-test-bot.commit.html.erb new file mode 100644 index 0000000000..c820263a76 --- /dev/null +++ b/Library/Contributions/cmds/brew-test-bot.commit.html.erb @@ -0,0 +1,24 @@ + + + + BrewBot: <%= @test.name %> + BrewBot + + + +
+

BrewBot

+

<%= @test.name %> <%= DateTime.now.strftime "%T %D" %>

+ + <% for step in @test.steps %> + + + + + <% end %> +
$ <%= step.command %><%= step.status_upcase %>
+
+ + diff --git a/Library/Contributions/cmds/brew-test-bot.css b/Library/Contributions/cmds/brew-test-bot.css new file mode 100644 index 0000000000..3fcefa59a2 --- /dev/null +++ b/Library/Contributions/cmds/brew-test-bot.css @@ -0,0 +1,75 @@ +body { + background: #27221a; + color: #f6e6cc; + font-family: Helvetica, Arial, sans-serif; + font-size: 1.5em; +} + +h1, h2 { + text-align: center; + font-weight: bold; +} + +h1 { + font-size: 3em; + padding-top: 0.5em; + padding-bottom: 0; + margin-bottom: 0; + text-shadow: 1px 1px 10px rgba(0,0,0,0.25); + color: #D7AF72; + letter-spacing: -3px; +} + +h2 { + font-size: 0.8em; + margin-top: 0; + padding-top: 0; + padding-bottom: 1em; + color: #A1804C; +} + +h1 a:link, h1 a:visited, h2 a:link, h2 a:visited { + text-decoration: none; +} + +table { + background-color: rgba(0, 0, 0, 0.30); + color: white; + border-radius: 0.4em; + padding-top: 1em; + padding-bottom: 1em; + font-size: inherit; + font-family: monospace; + list-style-type: none; + margin-left: auto; + margin-right: auto; +} + +td { + padding-left: 1em; + padding-right: 1em; +} + +.prompt { + color: #E3D796; +} + +.status { + text-align: right; +} + +.running a:link, .running a:visited { + color: orange; +} + +.passed a:link, .passed a:visited { + color: green; +} + +.failed a:link, .failed a:visited { + color: red; +} + +a:active, a:visited { + color: inherit; +} diff --git a/Library/Contributions/cmds/brew-test-bot.index.html.erb b/Library/Contributions/cmds/brew-test-bot.index.html.erb new file mode 100644 index 0000000000..d5c5d6913c --- /dev/null +++ b/Library/Contributions/cmds/brew-test-bot.index.html.erb @@ -0,0 +1,23 @@ + + + + BrewBot + + + +
+

BrewBot

+ + <% dirs.each_with_index do |dir,index| %> + + + + + + <% end %> +
<%= dir %><%= dates[index] %><%= statuses[index].upcase %>
+
+ + diff --git a/Library/Contributions/cmds/brew-test-bot.rb b/Library/Contributions/cmds/brew-test-bot.rb new file mode 100755 index 0000000000..3699bd7d95 --- /dev/null +++ b/Library/Contributions/cmds/brew-test-bot.rb @@ -0,0 +1,277 @@ +# Comprehensively test a formula or pull request. +# +# Usage: brew test-bot [options...] +# +# Options: +# --log: Writes log files under ./brewbot/ +# --html: Writes html and log files under ./brewbot/ +# --comment: Comment on the pull request +# --clean: Clean the Homebrew directory. Very dangerous. Use with care. + +require 'utils' +require 'date' + +HOMEBREW_CONTRIBUTED_CMDS = HOMEBREW_REPOSITORY + "Library/Contributions/cmds/" + +class Step + attr_reader :command + attr_accessor :status + + def initialize test, command + @test = test + @category = test.category + @command = command + @name = command.split[1].delete '-' + @status = :running + @test.steps << self + write_html + end + + def log_file_path full_path=true + return "/dev/null" unless ARGV.include? "--log" or ARGV.include? "--html" + 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 write_html + return unless @test.log_root and ARGV.include? "--html" + + open(@test.log_root + "index.html", "w") do |index| + commit_html, css = @test.commit_html_and_css + index.write commit_html.result binding + end + end + + def self.run test, command + step = new test, command + step.puts_command + `#{step.command} &>#{step.log_file_path}` + step.status = $?.success? ? :passed : :failed + step.puts_result + step.write_html + end +end + +class Test + attr_reader :log_root, :category, :name + attr_reader :core_changed, :formulae + attr_accessor :steps + + @@css = @@index_html = @@commit_html = nil + + def commit_html_and_css + return @@commit_html, @@css + end + + def initialize arg + begin + Formula.factory arg + rescue FormulaUnavailableError + ofail "#{arg} is not a pull request number or formula." unless arg.to_i > 0 + @url = arg + @formulae = [] + else + @url = nil + @formulae = [arg] + end + + @start_sha1 = nil + @category = __method__ + @steps = [] + @core_changed = false + @brewbot_root = Pathname.pwd + "brewbot" + FileUtils.mkdir_p @brewbot_root if ARGV.include? "--log" or ARGV.include? "--html" + + if ARGV.include? "--html" and not @@css + require 'erb' + root = HOMEBREW_CONTRIBUTED_CMDS + @@css = IO.read root + "brew-test-bot.css" + @@index_html = ERB.new IO.read root + "brew-test-bot.index.html.erb" + @@commit_html = ERB.new IO.read root + "brew-test-bot.commit.html.erb" + end + end + + def write_root_html status + return unless ARGV.include? "--html" + + FileUtils.mv Dir.glob("*.txt"), @log_root + open(@log_root + "status.txt", "w") do |file| + file.write status + end + + dirs = [] + dates = [] + statuses = [] + + Pathname.glob("#{@brewbot_root}/*/status.txt").each do |result| + dirs << result.dirname.basename + status_file = result.dirname + "status.txt" + dates << File.mtime(status_file).strftime("%T %D") + statuses << IO.read(status_file) + end + + open(@brewbot_root + "index.html", "w") do |index| + css = @@css + index.write @@index_html.result binding + end + end + + def download + def current_sha1 + `git rev-parse --short HEAD`.strip + end + + def current_branch + `git symbolic-ref HEAD`.slice!("refs/heads/").strip + end + + @category = __method__ + if @url + `git am --abort 2>/dev/null` + test "brew update" if current_branch == "master" + @start_sha1 = current_sha1 + test "brew pull --clean #{@url}" + end_sha1 = current_sha1 + else + @start_sha1 = end_sha1 = current_sha1 + end + + name_prefix = @url ? @url : @formulae.first + @name = "#{name_prefix}-#{end_sha1}" + @log_root = @brewbot_root + @name + FileUtils.mkdir_p @log_root if ARGV.include? "--log" or ARGV.include? "--html" + + write_root_html :running + + return unless @url and @start_sha1 != end_sha1 and steps.last.status == :passed + + `git diff #{@start_sha1}..#{end_sha1} --name-status`.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? 'bin/brew' + @homebrew_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}" + + test "brew audit #{formula}" + test "brew install --verbose --build-bottle #{formula}" + return unless steps.last.status == :passed + test "brew test #{formula}" + test "brew bottle #{formula}" + test "brew uninstall #{formula}" + end + + def homebrew + @category = __method__ + test "brew tests" + end + + def cleanup + @category = __method__ + if ARGV.include? "--clean" + test "git reset --hard origin/master" + test "git clean --force -dx" + else + `git diff --exit-code HEAD 2>/dev/null` + ofail "Uncommitted changes, aborting." unless $?.success? + test "git reset --hard #{@start_sha1}" if @start_sha1 + end + end + + def test cmd + Step.run self, cmd + 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 + + write_root_html status + + if ARGV.include? "--comment" and @url + username, password = IO.read(File.expand_path('~/.brewbot')).split(':') + url = "https://api.github.com/repos/mxcl/homebrew/issues/#{@url}/comments" + require 'vendor/multi_json' + json = MultiJson.encode(:body => message) + curl url, "-X", "POST", "--user", "#{username}:#{password}", "--data", json, "-o", "/dev/null" + end + end + + def self.run url + test = new url + test.cleanup + test.download + test.setup + test.formulae.each do |f| + test.formula f + end + test.homebrew if test.core_changed + test.cleanup + + test.check_results + end +end + +if ARGV.empty? + ofail 'This command requires at least one argument containing a pull request number or formula.' +end + +Dir.chdir HOMEBREW_REPOSITORY + +ARGV.named.each do|arg| + Test.run arg +end