ci_reporter 1.0 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2006-2010 Nick Sieger <nicksieger@gmail.com>
2
+ # See the file LICENSE.txt included with the distribution for
3
+ # software license details.
4
+
5
+ module CI
6
+ module Reporter
7
+ def self.maybe_quote_filename(fn)
8
+ if fn =~ /\s/
9
+ fn = %{"#{fn}"}
10
+ end
11
+ fn
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,7 @@
1
+ # Copyright (c) 2006-2010 Nick Sieger <nicksieger@gmail.com>
2
+ # See the file LICENSE.txt included with the distribution for
3
+ # software license details.
4
+
1
5
  require 'fileutils'
2
6
 
3
7
  module CI #:nodoc:
@@ -10,10 +14,44 @@ module CI #:nodoc:
10
14
  end
11
15
 
12
16
  def write_report(suite)
13
- File.open("#{@basename}-#{suite.name.gsub(/[^a-zA-Z0-9]+/, '-')}.xml", "w") do |f|
17
+ File.open(filename_for(suite), "w") do |f|
14
18
  f << suite.to_xml
15
19
  end
16
20
  end
21
+
22
+ private
23
+
24
+
25
+ # creates a uniqe filename per suite
26
+ # to prevent results from being overwritten
27
+ # if a result file is already written, it appends an index
28
+ # e.g.
29
+ # SPEC-MailsController.xml
30
+ # SPEC-MailsController.0.xml
31
+ # SPEC-MailsController.1.xml
32
+ # SPEC-MailsController...xml
33
+ # SPEC-MailsController.N.xml
34
+ #
35
+ # with N < 100000, to prevent endless sidestep loops
36
+ MAX_SIDESTEPS = 100000
37
+ #
38
+ def filename_for(suite)
39
+ basename = "#{@basename}-#{suite.name.gsub(/[^a-zA-Z0-9]+/, '-')}"
40
+ suffix = "xml"
41
+
42
+ # the initial filename, e.g. SPEC-MailsController.xml
43
+ filename = [basename, suffix].join(".")
44
+
45
+ # if the initial filename is already in use
46
+ # do sidesteps, beginning with SPEC-MailsController.0.xml
47
+ i = 0
48
+ while File.exists?(filename) && i < MAX_SIDESTEPS
49
+ filename = [basename, i, suffix].join(".")
50
+ i += 1
51
+ end
52
+
53
+ filename
54
+ end
17
55
  end
18
56
  end
19
- end
57
+ end
@@ -1,13 +1,38 @@
1
+ # Copyright (c) 2006-2010 Nick Sieger <nicksieger@gmail.com>
2
+ # See the file LICENSE.txt included with the distribution for
3
+ # software license details.
4
+
1
5
  require 'ci/reporter/core'
2
- gem 'rspec'
3
- require 'spec'
4
6
 
5
7
  module CI
6
8
  module Reporter
9
+ module RSpecFormatters
10
+ begin
11
+ require 'rspec/core/formatters/base_formatter'
12
+ require 'rspec/core/formatters/progress_formatter'
13
+ require 'rspec/core/formatters/documentation_formatter'
14
+ BaseFormatter = ::RSpec::Core::Formatters::BaseFormatter
15
+ ProgressFormatter = ::RSpec::Core::Formatters::ProgressFormatter
16
+ DocFormatter = ::RSpec::Core::Formatters::DocumentationFormatter
17
+ rescue LoadError => first_error
18
+ begin
19
+ require 'spec/runner/formatter/progress_bar_formatter'
20
+ require 'spec/runner/formatter/specdoc_formatter'
21
+ BaseFormatter = ::Spec::Runner::Formatter::BaseFormatter
22
+ ProgressFormatter = ::Spec::Runner::Formatter::ProgressBarFormatter
23
+ DocFormatter = ::Spec::Runner::Formatter::SpecdocFormatter
24
+ rescue LoadError
25
+ raise first_error
26
+ end
27
+ end
28
+ end
29
+
7
30
  # Wrapper around a <code>RSpec</code> error or failure to be used by the test suite to interpret results.
8
31
  class RSpecFailure
32
+ attr_reader :exception
9
33
  def initialize(failure)
10
34
  @failure = failure
35
+ @exception = failure.exception
11
36
  end
12
37
 
13
38
  def failure?
@@ -15,71 +40,170 @@ module CI
15
40
  end
16
41
 
17
42
  def error?
18
- !@failure.expectation_not_met?
43
+ !failure?
44
+ end
45
+
46
+ def name() exception.class.name end
47
+ def message() exception.message end
48
+ def location() (exception.backtrace || ["No backtrace available"]).join("\n") end
49
+ end
50
+
51
+ class RSpec2Failure < RSpecFailure
52
+ def initialize(example, formatter)
53
+ @formatter = formatter
54
+ @example = example
55
+ @exception = @example.execution_result[:exception] || @example.execution_result[:exception_encountered]
56
+ end
57
+
58
+ def failure?
59
+ exception.is_a?(::RSpec::Expectations::ExpectationNotMetError)
19
60
  end
20
61
 
21
- def name() @failure.exception.class.name end
22
- def message() @failure.exception.message end
23
- def location() @failure.exception.backtrace.join("\n") end
62
+ def location()
63
+ output = []
64
+ output.push "#{exception.class.name << ":"}" unless exception.class.name =~ /RSpec/
65
+ output.push @exception.message
66
+
67
+ @formatter.format_backtrace(@exception.backtrace, @example).each do |backtrace_info|
68
+ output.push " #{backtrace_info}"
69
+ end
70
+ output.join "\n"
71
+ end
24
72
  end
25
73
 
26
74
  # Custom +RSpec+ formatter used to hook into the spec runs and capture results.
27
- class RSpec < Spec::Runner::Formatter::ProgressBarFormatter
28
- def initialize(output, dry_run=false, colour=false, report_mgr=nil)
29
- super(output, dry_run, colour)
30
- @report_manager = report_mgr || ReportManager.new("spec")
75
+ class RSpec < RSpecFormatters::BaseFormatter
76
+ attr_accessor :report_manager
77
+ attr_accessor :formatter
78
+ def initialize(*args)
79
+ super
80
+ @formatter ||= RSpecFormatters::ProgressFormatter.new(*args)
81
+ @report_manager = ReportManager.new("spec")
31
82
  @suite = nil
32
83
  end
33
84
 
34
85
  def start(spec_count)
35
- super
86
+ @formatter.start(spec_count)
36
87
  end
37
88
 
38
- def add_context(name, first)
39
- super
40
- write_report if @suite
41
- @suite = TestSuite.new name
42
- @suite.start
89
+ # rspec 0.9
90
+ def add_behaviour(name)
91
+ @formatter.add_behaviour(name)
92
+ new_suite(name)
43
93
  end
44
94
 
45
- def spec_started(name)
46
- super
47
- spec = TestCase.new name
95
+ # Compatibility with rspec < 1.2.4
96
+ def add_example_group(example_group)
97
+ @formatter.add_example_group(example_group)
98
+ new_suite(description_for(example_group))
99
+ end
100
+
101
+ # rspec >= 1.2.4
102
+ def example_group_started(example_group)
103
+ @formatter.example_group_started(example_group)
104
+ new_suite(description_for(example_group))
105
+ end
106
+
107
+ def example_group_finished(example_group)
108
+ @formatter.example_group_finished(example_group)
109
+ end
110
+
111
+ def example_started(name_or_example)
112
+ @formatter.example_started(name_or_example)
113
+ spec = TestCase.new
48
114
  @suite.testcases << spec
49
115
  spec.start
50
116
  end
51
117
 
52
- def spec_failed(name, counter, failure)
53
- super
118
+ def example_failed(name_or_example, *rest)
119
+ @formatter.example_failed(name_or_example, *rest)
120
+
121
+ # In case we fail in before(:all)
122
+ example_started(name_or_example) if @suite.testcases.empty?
123
+
124
+ if name_or_example.respond_to?(:execution_result) # RSpec 2
125
+ failure = RSpec2Failure.new(name_or_example, @formatter)
126
+ else
127
+ failure = RSpecFailure.new(rest[1]) # example_failed(name, counter, failure) in RSpec 1
128
+ end
129
+
54
130
  spec = @suite.testcases.last
55
131
  spec.finish
56
- spec.failure = RSpecFailure.new(failure)
132
+ spec.name = description_for(name_or_example)
133
+ spec.failures << failure
57
134
  end
58
135
 
59
- def spec_passed(name)
60
- super
136
+ def example_passed(name_or_example)
137
+ @formatter.example_passed(name_or_example)
138
+ spec = @suite.testcases.last
139
+ spec.finish
140
+ spec.name = description_for(name_or_example)
141
+ end
142
+
143
+ def example_pending(*args)
144
+ @formatter.example_pending(*args)
145
+ name = description_for(args[0])
61
146
  spec = @suite.testcases.last
62
147
  spec.finish
148
+ spec.name = "#{name} (PENDING)"
149
+ spec.skipped = true
63
150
  end
64
151
 
65
152
  def start_dump
66
- super
153
+ @formatter.start_dump
67
154
  end
68
155
 
69
- def dump_failure(counter, failure)
70
- super
156
+ def dump_failures(*args)
157
+ @formatter.dump_failures(*args)
71
158
  end
72
159
 
73
- def dump_summary(duration, spec_count, failure_count)
74
- super
160
+ def dump_failure(*args)
161
+ @formatter.dump_failure(*args)
162
+ end
163
+
164
+ def dump_summary(*args)
165
+ @formatter.dump_summary(*args)
75
166
  write_report
76
167
  end
77
168
 
169
+ def dump_pending
170
+ @formatter.dump_pending
171
+ end
172
+
173
+ def close
174
+ @formatter.close
175
+ end
176
+
78
177
  private
178
+ def description_for(name_or_example)
179
+ if name_or_example.respond_to?(:full_description)
180
+ name_or_example.full_description
181
+ elsif name_or_example.respond_to?(:metadata)
182
+ name_or_example.metadata[:example_group][:full_description]
183
+ elsif name_or_example.respond_to?(:description)
184
+ name_or_example.description
185
+ else
186
+ "UNKNOWN"
187
+ end
188
+ end
189
+
79
190
  def write_report
80
191
  @suite.finish
81
192
  @report_manager.write_report(@suite)
82
193
  end
194
+
195
+ def new_suite(name)
196
+ write_report if @suite
197
+ @suite = TestSuite.new name
198
+ @suite.start
199
+ end
200
+ end
201
+
202
+ class RSpecDoc < RSpec
203
+ def initialize(*args)
204
+ @formatter = RSpecFormatters::DocFormatter.new(*args)
205
+ super
206
+ end
83
207
  end
84
208
  end
85
- end
209
+ end
@@ -1,39 +1,72 @@
1
+ # Copyright (c) 2006-2010 Nick Sieger <nicksieger@gmail.com>
2
+ # See the file LICENSE.txt included with the distribution for
3
+ # software license details.
4
+
5
+ require 'delegate'
6
+ require 'stringio'
7
+
1
8
  module CI
2
9
  module Reporter
10
+ # Emulates/delegates IO to $stdout or $stderr in order to capture output to report in the XML file.
11
+ class OutputCapture < DelegateClass(IO)
12
+ # Start capturing IO, using the given block to assign self to the proper IO global.
13
+ def initialize(io, &assign)
14
+ super
15
+ @delegate_io = io
16
+ @captured_io = StringIO.new
17
+ @assign_block = assign
18
+ @assign_block.call self
19
+ end
20
+
21
+ # Finalize the capture and reset to the original IO object.
22
+ def finish
23
+ @assign_block.call @delegate_io
24
+ @captured_io.string
25
+ end
26
+
27
+ # setup tee methods
28
+ %w(<< print printf putc puts write).each do |m|
29
+ module_eval(<<-EOS, __FILE__, __LINE__)
30
+ def #{m}(*args, &block)
31
+ @delegate_io.send(:#{m}, *args, &block)
32
+ @captured_io.send(:#{m}, *args, &block)
33
+ end
34
+ EOS
35
+ end
36
+ end
37
+
3
38
  # Basic structure representing the running of a test suite. Used to time tests and store results.
4
- class TestSuite < Struct.new(:name, :tests, :time, :failures, :errors)
39
+ class TestSuite < Struct.new(:name, :tests, :time, :failures, :errors, :skipped, :assertions)
5
40
  attr_accessor :testcases
41
+ attr_accessor :stdout, :stderr
6
42
  def initialize(name)
7
- super
43
+ super(name.to_s) # RSpec passes a "description" object instead of a string
8
44
  @testcases = []
9
45
  end
10
46
 
11
47
  # Starts timing the test suite.
12
48
  def start
13
49
  @start = Time.now
50
+ unless ENV['CI_CAPTURE'] == "off"
51
+ @capture_out = OutputCapture.new($stdout) {|io| $stdout = io }
52
+ @capture_err = OutputCapture.new($stderr) {|io| $stderr = io }
53
+ end
14
54
  end
15
55
 
16
56
  # Finishes timing the test suite.
17
57
  def finish
18
58
  self.tests = testcases.size
19
59
  self.time = Time.now - @start
20
- self.failures = testcases.select {|tc| tc.failure? }.size
21
- self.errors = testcases.select {|tc| tc.error? }.size
60
+ self.failures = testcases.inject(0) {|sum,tc| sum += tc.failures.select{|f| f.failure? }.size }
61
+ self.errors = testcases.inject(0) {|sum,tc| sum += tc.failures.select{|f| f.error? }.size }
62
+ self.skipped = testcases.inject(0) {|sum,tc| sum += (tc.skipped? ? 1 : 0) }
63
+ self.stdout = @capture_out.finish if @capture_out
64
+ self.stderr = @capture_err.finish if @capture_err
22
65
  end
23
66
 
24
67
  # Creates the xml builder instance used to create the report xml document.
25
68
  def create_builder
26
- begin
27
- gem 'builder'
28
- require 'builder'
29
- rescue
30
- begin
31
- gem 'activesupport'
32
- require 'active_support'
33
- rescue
34
- raise LoadError, "XML Builder is required by CI::Reporter"
35
- end
36
- end unless defined?(Builder::XmlMarkup)
69
+ require 'builder'
37
70
  # :escape_attrs is obsolete in a newer version, but should do no harm
38
71
  Builder::XmlMarkup.new(:indent => 2, :escape_attrs => true)
39
72
  end
@@ -42,29 +75,35 @@ module CI
42
75
  def to_xml
43
76
  builder = create_builder
44
77
  # more recent version of Builder doesn't need the escaping
45
- if Builder::XmlMarkup.private_instance_methods.include?("_attr_value")
46
- def builder.trunc!(txt)
47
- txt.sub(/\n.*/m, '...')
48
- end
49
- else
50
- def builder.trunc!(txt)
51
- _escape(txt.sub(/\n.*/m, '...'))
52
- end
78
+ def builder.trunc!(txt)
79
+ txt.sub(/\n.*/m, '...')
53
80
  end
54
81
  builder.instruct!
55
82
  attrs = {}
56
- each_pair {|k,v| attrs[k] = builder.trunc!(v.to_s) }
83
+ each_pair {|k,v| attrs[k] = builder.trunc!(v.to_s) unless v.nil? || v.to_s.empty? }
57
84
  builder.testsuite(attrs) do
58
85
  @testcases.each do |tc|
59
86
  tc.to_xml(builder)
60
87
  end
88
+ builder.tag! "system-out" do
89
+ builder.text! self.stdout
90
+ end
91
+ builder.tag! "system-err" do
92
+ builder.text! self.stderr
93
+ end
61
94
  end
62
95
  end
63
96
  end
64
97
 
65
98
  # Structure used to represent an individual test case. Used to time the test and store the result.
66
- class TestCase < Struct.new(:name, :time)
67
- attr_accessor :failure
99
+ class TestCase < Struct.new(:name, :time, :assertions)
100
+ attr_accessor :failures
101
+ attr_accessor :skipped
102
+
103
+ def initialize(*args)
104
+ super
105
+ @failures = []
106
+ end
68
107
 
69
108
  # Starts timing the test.
70
109
  def start
@@ -78,26 +117,35 @@ module CI
78
117
 
79
118
  # Returns non-nil if the test failed.
80
119
  def failure?
81
- failure && failure.failure?
120
+ !failures.empty? && failures.detect {|f| f.failure? }
82
121
  end
83
122
 
84
123
  # Returns non-nil if the test had an error.
85
124
  def error?
86
- failure && failure.error?
125
+ !failures.empty? && failures.detect {|f| f.error? }
126
+ end
127
+
128
+ def skipped?
129
+ return skipped
87
130
  end
88
131
 
89
132
  # Writes xml representing the test result to the provided builder.
90
133
  def to_xml(builder)
91
134
  attrs = {}
92
- each_pair {|k,v| attrs[k] = builder.trunc!(v.to_s) }
135
+ each_pair {|k,v| attrs[k] = builder.trunc!(v.to_s) unless v.nil? || v.to_s.empty?}
93
136
  builder.testcase(attrs) do
94
- if failure
95
- builder.failure(:type => builder.trunc!(failure.name), :message => builder.trunc!(failure.message)) do
96
- builder.text!(failure.location)
137
+ if skipped
138
+ builder.skipped
139
+ else
140
+ failures.each do |failure|
141
+ builder.failure(:type => builder.trunc!(failure.name), :message => builder.trunc!(failure.message)) do
142
+ builder.text!(failure.message + " (#{failure.name})\n")
143
+ builder.text!(failure.location)
144
+ end
97
145
  end
98
146
  end
99
147
  end
100
148
  end
101
149
  end
102
150
  end
103
- end
151
+ end
@@ -1,3 +1,7 @@
1
+ # Copyright (c) 2006-2010 Nick Sieger <nicksieger@gmail.com>
2
+ # See the file LICENSE.txt included with the distribution for
3
+ # software license details.
4
+
1
5
  require 'ci/reporter/core'
2
6
  require 'test/unit'
3
7
  require 'test/unit/ui/console/testrunner'
@@ -8,15 +12,16 @@ module CI
8
12
  # of the test.
9
13
  class Failure
10
14
  def self.new(fault)
11
- fault.kind_of?(Test::Unit::Failure) ? TestUnitFailure.new(fault) : TestUnitError.new(fault)
15
+ return TestUnitFailure.new(fault) if fault.kind_of?(Test::Unit::Failure)
16
+ return TestUnitSkipped.new(fault) if Test::Unit.constants.include?("Omission") && (fault.kind_of?(Test::Unit::Omission) || fault.kind_of?(Test::Unit::Pending))
17
+ return TestUnitNotification.new(fault) if Test::Unit.constants.include?("Notification") && fault.kind_of?(Test::Unit::Notification)
18
+ TestUnitError.new(fault)
12
19
  end
13
20
  end
14
21
 
15
22
  # Wrapper around a <code>Test::Unit</code> error to be used by the test suite to interpret results.
16
23
  class TestUnitError
17
- def initialize(fault)
18
- @fault = fault
19
- end
24
+ def initialize(fault) @fault = fault end
20
25
  def failure?() false end
21
26
  def error?() true end
22
27
  def name() @fault.exception.class.name end
@@ -26,9 +31,7 @@ module CI
26
31
 
27
32
  # Wrapper around a <code>Test::Unit</code> failure to be used by the test suite to interpret results.
28
33
  class TestUnitFailure
29
- def initialize(fault)
30
- @fault = fault
31
- end
34
+ def initialize(fault) @fault = fault end
32
35
  def failure?() true end
33
36
  def error?() false end
34
37
  def name() Test::Unit::AssertionFailedError.name end
@@ -36,6 +39,26 @@ module CI
36
39
  def location() @fault.location.join("\n") end
37
40
  end
38
41
 
42
+ # Wrapper around a <code>Test::Unit</code> 2.0 omission.
43
+ class TestUnitSkipped
44
+ def initialize(fault) @fault = fault end
45
+ def failure?() false end
46
+ def error?() false end
47
+ def name() Test::Unit::Omission.name end
48
+ def message() @fault.message end
49
+ def location() @fault.location.join("\n") end
50
+ end
51
+
52
+ # Wrapper around a <code>Test::Unit</code> 2.0 notification.
53
+ class TestUnitNotification
54
+ def initialize(fault) @fault = fault end
55
+ def failure?() false end
56
+ def error?() false end
57
+ def name() Test::Unit::Notification.name end
58
+ def message() @fault.message end
59
+ def location() @fault.location.join("\n") end
60
+ end
61
+
39
62
  # Replacement Mediator that adds listeners to capture the results of the <code>Test::Unit</code> runs.
40
63
  class TestUnit < Test::Unit::UI::TestRunnerMediator
41
64
  def initialize(suite, report_mgr = nil)
@@ -49,8 +72,11 @@ module CI
49
72
  end
50
73
 
51
74
  def started(result)
75
+ @suite_result = result
76
+ @last_assertion_count = 0
52
77
  @current_suite = nil
53
78
  @unknown_count = 0
79
+ @result_assertion_count = 0
54
80
  end
55
81
 
56
82
  def test_started(name)
@@ -67,7 +93,8 @@ module CI
67
93
  end
68
94
 
69
95
  def fault(fault)
70
- finish_test(fault)
96
+ tc = @current_suite.testcases.last
97
+ tc.failures << Failure.new(fault)
71
98
  end
72
99
 
73
100
  def finished(elapsed_time)
@@ -92,7 +119,9 @@ module CI
92
119
 
93
120
  def finish_suite
94
121
  if @current_suite
95
- @current_suite.finish
122
+ @current_suite.finish
123
+ @current_suite.assertions = @suite_result.assertion_count - @last_assertion_count
124
+ @last_assertion_count = @suite_result.assertion_count
96
125
  @report_manager.write_report(@current_suite)
97
126
  end
98
127
  end
@@ -103,11 +132,12 @@ module CI
103
132
  @current_suite.testcases << tc
104
133
  end
105
134
 
106
- def finish_test(failure = nil)
135
+ def finish_test
107
136
  tc = @current_suite.testcases.last
108
137
  tc.finish
109
- tc.failure = Failure.new(failure) if failure
138
+ tc.assertions = @suite_result.assertion_count - @result_assertion_count
139
+ @result_assertion_count = @suite_result.assertion_count
110
140
  end
111
141
  end
112
142
  end
113
- end
143
+ end
@@ -0,0 +1,5 @@
1
+ module CI
2
+ module Reporter
3
+ VERSION = "1.6.5"
4
+ end
5
+ end