Skip to content
This repository
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 172 lines (151 sloc) 5.539 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
# Copyright (c) 2006-2012 Nick Sieger <nicksieger@gmail.com>
# See the file LICENSE.txt included with the distribution for
# software license details.

require 'delegate'
require 'stringio'

module CI
  module Reporter
    # Emulates/delegates IO to $stdout or $stderr in order to capture output to report in the XML file.
    module OutputCapture
      class Delegate < DelegateClass(IO)
        include OutputCapture
        def initialize(io, &assign)
          super(io)
          capture(io, &assign)
        end
      end

      def self.wrap(io, &assign)
        if defined?(RUBY_ENGINE) # JRuby, Ruby 1.9, etc.
          Delegate.new(io, &assign)
        else # Ruby 1.8 requires streams to be subclass of IO
          IO.new(io.fileno, "w").tap {|x| x.extend self; x.capture(io, &assign) }
        end
      end

      # Start capturing IO, using the given block to assign self to the proper IO global.
      def capture(io, &assign)
        @delegate_io = io
        @captured_io = StringIO.new
        @assign_block = assign
        @assign_block.call self
      end

      # Finalize the capture and reset to the original IO object.
      def finish
        @assign_block.call @delegate_io
        @captured_io.string
      end

      # setup tee methods
      %w(<< print printf putc puts write).each do |m|
        module_eval(<<-EOS, __FILE__, __LINE__)
def #{m}(*args, &block)
@delegate_io.send(:#{m}, *args, &block)
@captured_io.send(:#{m}, *args, &block)
end
EOS
      end
    end

    # Basic structure representing the running of a test suite. Used to time tests and store results.
    class TestSuite < Struct.new(:name, :tests, :time, :failures, :errors, :skipped, :assertions)
      attr_accessor :testcases
      attr_accessor :stdout, :stderr
      def initialize(name)
        super(name.to_s) # RSpec passes a "description" object instead of a string
        @testcases = []
      end

      # Starts timing the test suite.
      def start
        @start = Time.now
        unless ENV['CI_CAPTURE'] == "off"
          @capture_out = OutputCapture.wrap($stdout) {|io| $stdout = io }
          @capture_err = OutputCapture.wrap($stderr) {|io| $stderr = io }
        end
      end

      # Finishes timing the test suite.
      def finish
        self.tests = testcases.size
        self.time = Time.now - @start
        self.failures = testcases.inject(0) {|sum,tc| sum += tc.failures.select{|f| f.failure? }.size }
        self.errors = testcases.inject(0) {|sum,tc| sum += tc.failures.select{|f| f.error? }.size }
        self.skipped = testcases.inject(0) {|sum,tc| sum += (tc.skipped? ? 1 : 0) }
        self.stdout = @capture_out.finish if @capture_out
        self.stderr = @capture_err.finish if @capture_err
      end

      # Creates the xml builder instance used to create the report xml document.
      def create_builder
        require 'builder'
        # :escape_attrs is obsolete in a newer version, but should do no harm
        Builder::XmlMarkup.new(:indent => 2, :escape_attrs => true)
      end

      # Creates an xml string containing the test suite results.
      def to_xml
        builder = create_builder
        # more recent version of Builder doesn't need the escaping
        def builder.trunc!(txt)
          txt.sub(/\n.*/m, '...')
        end
        builder.instruct!
        attrs = {}
        each_pair {|k,v| attrs[k] = builder.trunc!(v.to_s) unless v.nil? || v.to_s.empty? }
        builder.testsuite(attrs) do
          @testcases.each do |tc|
            tc.to_xml(builder)
          end
          builder.tag! "system-out" do
            builder.text!(self.stdout || '' )
          end
          builder.tag! "system-err" do
            builder.text!(self.stderr || '' )
          end
        end
      end
    end

    # Structure used to represent an individual test case. Used to time the test and store the result.
    class TestCase < Struct.new(:name, :time, :assertions)
      attr_accessor :failures
      attr_accessor :skipped

      def initialize(*args)
        super
        @failures = []
      end

      # Starts timing the test.
      def start
        @start = Time.now
      end

      # Finishes timing the test.
      def finish
        self.time = Time.now - @start
      end

      # Returns non-nil if the test failed.
      def failure?
        !failures.empty? && failures.detect {|f| f.failure? }
      end

      # Returns non-nil if the test had an error.
      def error?
        !failures.empty? && failures.detect {|f| f.error? }
      end

      def skipped?
        return skipped
      end

      # Writes xml representing the test result to the provided builder.
      def to_xml(builder)
        attrs = {}
        each_pair {|k,v| attrs[k] = builder.trunc!(v.to_s) unless v.nil? || v.to_s.empty?}
        builder.testcase(attrs) do
          if skipped
            builder.skipped
          else
            failures.each do |failure|
              tag = case failure.class.name
                    when /TestUnitSkipped/ then :skipped
                    when /TestUnitError/, /MiniTestError/ then :error
                    else :failure end

              builder.tag!(tag, :type => builder.trunc!(failure.name), :message => builder.trunc!(failure.message)) do
                builder.text!(failure.message + " (#{failure.name})\n")
                builder.text!(failure.location)
              end
            end
          end
        end
      end
    end
  end
end
Something went wrong with that request. Please try again.