mcmire-protest 0.2.4

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,248 @@
1
+ module Protest
2
+ # Define a top level test context where to define tests. This works exactly
3
+ # the same as subclassing TestCase explicitly.
4
+ #
5
+ # Protest.context "A user" do
6
+ # ...
7
+ # end
8
+ #
9
+ # is just syntax sugar to write:
10
+ #
11
+ # class TestUser < Protest::TestCase
12
+ # self.description = "A user"
13
+ # ...
14
+ # end
15
+ def self.context(description, &block)
16
+ TestCase.context(description, &block)
17
+ end
18
+
19
+ class << self
20
+ alias_method :describe, :context
21
+ alias_method :story, :context
22
+ end
23
+
24
+ # A TestCase defines a suite of related tests. You can further categorize
25
+ # your tests by declaring nested contexts inside the class. See
26
+ # TestCase.context.
27
+ class TestCase
28
+ begin
29
+ require "test/unit/assertions"
30
+ include Test::Unit::Assertions
31
+ rescue LoadError
32
+ end
33
+
34
+ # Run all tests in this context. Takes a Report instance in order to
35
+ # provide output.
36
+ def self.run(runner)
37
+ runner.report(TestWrapper.new(:setup, self), true)
38
+ tests.each {|test| runner.report(test, false) }
39
+ runner.report(TestWrapper.new(:teardown, self), true)
40
+ rescue Exception => e
41
+ # If any exception bubbles up here, then it means it was during the
42
+ # global setup/teardown blocks, so let's just skip the rest of this
43
+ # context.
44
+ return
45
+ end
46
+
47
+ # Tests added to this context.
48
+ def self.tests
49
+ @tests ||= []
50
+ end
51
+
52
+ # Add a test to be run in this context. This method is aliased as +it+ and
53
+ # +should+ for your comfort.
54
+ def self.test(name, &block)
55
+ tests << new(name, caller.at(0), &block)
56
+ end
57
+
58
+ # Add a setup block to be run before each test in this context. This method
59
+ # is aliased as +before+ for your comfort.
60
+ def self.setup(&block)
61
+ define_method :setup do
62
+ super()
63
+ instance_eval(&block)
64
+ end
65
+ end
66
+
67
+ # Add a +setup+ block that will be run *once* for the entire test case,
68
+ # before the first test is run.
69
+ #
70
+ # Keep in mind that while +setup+ blocks are evaluated on the context of the
71
+ # test, and thus you can share state between them, your tests will not be
72
+ # able to access instance variables set in a +global_setup+ block.
73
+ #
74
+ # This is usually not needed (and generally using it is a code smell, since
75
+ # you could make a test dependent on the state of other tests, which is a
76
+ # huge problem), but it comes in handy when you need to do expensive
77
+ # operations in your test setup/teardown and the tests won't modify the
78
+ # state set on this operations. For example, creating large amount of
79
+ # records in a database or filesystem, when your tests will only read these
80
+ # records.
81
+ def self.global_setup(&block)
82
+ (class << self; self; end).class_eval do
83
+ define_method :do_global_setup do
84
+ super()
85
+ instance_eval(&block)
86
+ end
87
+ end
88
+ end
89
+
90
+ # Add a teardown block to be run after each test in this context. This
91
+ # method is aliased as +after+ for your comfort.
92
+ def self.teardown(&block)
93
+ define_method :teardown do
94
+ instance_eval(&block)
95
+ super()
96
+ end
97
+ end
98
+
99
+ # Add a +teardown+ block that will be run *once* for the entire test case,
100
+ # after the last test is run.
101
+ #
102
+ # Keep in mind that while +teardown+ blocks are evaluated on the context of
103
+ # the test, and thus you can share state between the tests and the
104
+ # teardown blocks, you will not be able to access instance variables set in
105
+ # a test from your +global_teardown+ block.
106
+ #
107
+ # See TestCase.global_setup for a discussion on why these methods are best
108
+ # avoided unless you really need them and use them carefully.
109
+ def self.global_teardown(&block)
110
+ (class << self; self; end).class_eval do
111
+ define_method :do_global_teardown do
112
+ instance_eval(&block)
113
+ super()
114
+ end
115
+ end
116
+ end
117
+
118
+ # Define a new test context nested under the current one. All +setup+ and
119
+ # +teardown+ blocks defined on the current context will be inherited by the
120
+ # new context. This method is aliased as +describe+ for your comfort.
121
+ def self.context(description, &block)
122
+ subclass = Class.new(self)
123
+ subclass.class_eval(&block) if block
124
+ subclass.description = description
125
+ const_set(sanitize_description(description), subclass)
126
+ end
127
+
128
+ class << self
129
+ # Fancy name for your test case, reports can use this to give nice,
130
+ # descriptive output when running your tests.
131
+ attr_accessor :description
132
+
133
+ alias_method :describe, :context
134
+ alias_method :story, :context
135
+
136
+ alias_method :before, :setup
137
+ alias_method :after, :teardown
138
+
139
+ alias_method :before_all, :global_setup
140
+ alias_method :after_all, :global_setup
141
+
142
+ alias_method :it, :test
143
+ alias_method :should, :test
144
+ alias_method :scenario, :test
145
+ end
146
+
147
+ # Initialize a new instance of a single test. This test can be run in
148
+ # isolation by calling TestCase#run.
149
+ def initialize(name, location, &block)
150
+ @test = block
151
+ @location = location
152
+ @name = name
153
+ end
154
+
155
+ # Run a test in isolation. Any +setup+ and +teardown+ blocks defined for
156
+ # this test case will be run as expected.
157
+ #
158
+ # You need to provide a Runner instance to handle errors/pending tests/etc.
159
+ #
160
+ # If the test's block is nil, then the test will be marked as pending and
161
+ # nothing will be run.
162
+ def run(report)
163
+ @report = report
164
+ pending if test.nil?
165
+
166
+ setup
167
+ instance_eval(&test)
168
+ teardown
169
+ @report = nil
170
+ end
171
+
172
+ # Ensure a condition is met. This will raise AssertionFailed if the
173
+ # condition isn't met. You can override the default failure message
174
+ # by passing it as an argument.
175
+ def assert(condition, message="Expected condition to be satisfied")
176
+ @report.add_assertion
177
+ raise AssertionFailed, message unless condition
178
+ end
179
+
180
+ # Provided for Test::Unit compatibility, this lets you include
181
+ # Test::Unit::Assertions and everything works seamlessly.
182
+ def assert_block(message="Expected condition to be satisified") #:nodoc:
183
+ assert(yield, message)
184
+ end
185
+
186
+ # Make the test be ignored as pending. You can override the default message
187
+ # that will be sent to the report by passing it as an argument.
188
+ def pending(message="Not Yet Implemented")
189
+ raise Pending, message, [@location, *caller].uniq
190
+ end
191
+
192
+ # Name of the test
193
+ def name
194
+ @name
195
+ end
196
+
197
+ private
198
+
199
+ def setup #:nodoc:
200
+ end
201
+
202
+ def teardown #:nodoc:
203
+ end
204
+
205
+ def test
206
+ @test
207
+ end
208
+
209
+ def self.sanitize_description(description)
210
+ "Test#{description.gsub(/\W+/, ' ').strip.gsub(/(^| )(\w)/) { $2.upcase }}".to_sym
211
+ end
212
+ private_class_method :sanitize_description
213
+
214
+ def self.do_global_setup
215
+ end
216
+ private_class_method :do_global_setup
217
+
218
+ def self.do_global_teardown
219
+ end
220
+ private_class_method :do_global_teardown
221
+
222
+ def self.description #:nodoc:
223
+ parent = ancestors[1..-1].detect {|a| a < Protest::TestCase }
224
+ "#{parent.description rescue nil} #{@description}".strip
225
+ end
226
+
227
+ def self.inherited(child)
228
+ Protest.add_test_case(child)
229
+ end
230
+
231
+ # Provides the TestCase API for global setup/teardown blocks, so they can be
232
+ # "faked" as tests into the reporter (they aren't counted towards the total
233
+ # number of tests but they could count towards the number of failures/errors.)
234
+ class TestWrapper #:nodoc:
235
+ attr_reader :name
236
+
237
+ def initialize(type, test_case)
238
+ @type = type
239
+ @test = test_case
240
+ @name = "Global #{@type} for #{test_case.description}"
241
+ end
242
+
243
+ def run(report)
244
+ @test.send("do_global_#{@type}")
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,90 @@
1
+ module Protest
2
+ # Encapsulates the relevant information about a test. Useful for certain
3
+ # reports.
4
+ class Test
5
+ # Instance of the test case that was run.
6
+ attr_reader :test
7
+
8
+ # Name of the test that passed. Useful for certain reports.
9
+ attr_reader :test_name
10
+
11
+ def initialize(test) #:nodoc:
12
+ @test = test
13
+ @test_name = test.name
14
+ end
15
+ end
16
+
17
+ # Mixin for tests that had an error (this could be either a failed assertion,
18
+ # unrescued exceptions, or just a pending tests.)
19
+ module TestWithErrors
20
+ # The triggered exception (AssertionFailed, Pending, or any
21
+ # subclass of Exception in the case of an ErroredTest.)
22
+ attr_reader :error
23
+
24
+ # Message with which it failed the assertion.
25
+ def error_message
26
+ error.message
27
+ end
28
+
29
+ # Line of the file where the assertion failed.
30
+ def line
31
+ backtrace.first.split(":")[1]
32
+ end
33
+
34
+ # File where the assertion failed.
35
+ def file
36
+ backtrace.first.split(":")[0]
37
+ end
38
+
39
+ # Filtered backtrace of the assertion. See Protest::Utils::BacktraceFilter
40
+ # for details on the filtering.
41
+ def backtrace
42
+ @backtrace ||= Protest.backtrace_filter.filter_backtrace(raw_backtrace)
43
+ end
44
+
45
+ # Raw backtrace, as provided by the error.
46
+ def raw_backtrace
47
+ error.backtrace
48
+ end
49
+ end
50
+
51
+ # Encapsulate the relevant information for a test that passed.
52
+ class PassedTest < Test
53
+ end
54
+
55
+ # Encapsulates the relevant information for a test which failed an
56
+ # assertion.
57
+ class FailedTest < Test
58
+ include TestWithErrors
59
+
60
+ def initialize(test, error) #:nodoc:
61
+ super(test)
62
+ @error = error
63
+ end
64
+ end
65
+
66
+ # Encapsulates the relevant information for a test which raised an
67
+ # unrescued exception.
68
+ class ErroredTest < Test
69
+ include TestWithErrors
70
+
71
+ def initialize(test, error) #:nodoc:
72
+ super(test)
73
+ @error = error
74
+ end
75
+ end
76
+
77
+ # Encapsulates the relevant information for a test that the user marked as
78
+ # pending.
79
+ class PendingTest < Test
80
+ include TestWithErrors
81
+
82
+ # Message passed to TestCase#pending, if any.
83
+ alias_method :pending_message, :error_message
84
+
85
+ def initialize(test, error) #:nodoc:
86
+ super(test)
87
+ @error = error
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,25 @@
1
+ module Protest
2
+ module Utils
3
+ # Small utility object to filter an error's backtrace and remove any mention
4
+ # of Protest's own files.
5
+ class BacktraceFilter
6
+ ESCAPE_PATHS = [
7
+ # Path to the library's 'lib' dir.
8
+ /^#{Regexp.escape(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__)))))}/,
9
+
10
+ # Users certainly don't care about what test loader is being used
11
+ %r[lib/rake/rake_test_loader.rb], %r[bin/testrb]
12
+ ]
13
+
14
+ # Filter the backtrace, removing any reference to files located in
15
+ # BASE_PATH.
16
+ def filter_backtrace(backtrace, prefix=nil)
17
+ paths = ESCAPE_PATHS + [prefix].compact
18
+ backtrace.reject do |line|
19
+ file = line.split(":").first
20
+ paths.any? {|path| File.expand_path(file) =~ path }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ module Protest
2
+ module Utils
3
+ # Mixin that provides colorful output to your console based reports. This uses
4
+ # bash's escape sequences, so it won't work on windows.
5
+ #
6
+ # TODO: Make this work on windows with ansicolor or whatever the gem is named
7
+ module ColorfulOutput
8
+ # Returns a hash with the color values for different states. Override this
9
+ # method safely to change the output colors. The defaults are:
10
+ #
11
+ # :passed:: Light green
12
+ # :pending:: Light yellow
13
+ # :errored:: Light purple
14
+ # :failed:: Light red
15
+ #
16
+ # See http://www.hypexr.org/bash_tutorial.php#colors for a description of
17
+ # Bash color codes.
18
+ def self.colors
19
+ { :passed => "1;32",
20
+ :pending => "1;33",
21
+ :errored => "1;35",
22
+ :failed => "1;31" }
23
+ end
24
+
25
+ class << self
26
+ # Whether to use colors in the output or not. The default is +true+.
27
+ attr_accessor :colorize
28
+ end
29
+
30
+ self.colorize = true
31
+
32
+ # Print the string followed by a newline to whatever IO stream is defined in
33
+ # the method #stream using the correct color depending on the state passed.
34
+ def puts(string=nil, state=:normal)
35
+ if string.nil? # calling IO#puts with nil is not the same as with no args
36
+ stream.puts
37
+ else
38
+ stream.puts colorize(string, state)
39
+ end
40
+ end
41
+
42
+ # Print the string to whatever IO stream is defined in the method #stream
43
+ # using the correct color depending on the state passed.
44
+ def print(string=nil, state=:normal)
45
+ if string.nil? # calling IO#puts with nil is not the same as with no args
46
+ stream.print
47
+ else
48
+ stream.print colorize(string, state)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def colorize(string, state)
55
+ if state == :normal || !ColorfulOutput.colorize
56
+ string
57
+ else
58
+ "\033[#{color_for_state(state)}m#{string}\033[0m"
59
+ end
60
+ end
61
+
62
+ def color_for_state(state)
63
+ ColorfulOutput.colors.fetch(state)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,129 @@
1
+ module Protest
2
+ module Utils
3
+ # Mixin that provides summaries for your text based test runs.
4
+ module Summaries
5
+ # Call on +:end+ to output the amount of tests (passed, pending, failed
6
+ # and errored), the amount of assertions, and the time elapsed.
7
+ #
8
+ # For example:
9
+ #
10
+ # on :end do |report|
11
+ # report.puts
12
+ # report.summarize_test_totals
13
+ # end
14
+ #
15
+ # This relies on the public Report API, and on the presence of a #puts
16
+ # method to write to whatever source you are writing your report.
17
+ def summarize_test_totals
18
+ puts test_totals
19
+ puts running_time
20
+ end
21
+
22
+ # Call on +:end+ to print a list of pending tests, including file and line
23
+ # number where the call to TestCase#pending+ was made.
24
+ #
25
+ # It will not output anything if there weren't any pending tests.
26
+ #
27
+ # For example:
28
+ #
29
+ # on :end do |report|
30
+ # report.puts
31
+ # report.summarize_pending_tests
32
+ # end
33
+ #
34
+ # This relies on the public Report API, and on the presence of a #puts
35
+ # method to write to whatever source you are writing your report.
36
+ def summarize_pending_tests
37
+ return if pendings.empty?
38
+
39
+ puts "Pending tests:"
40
+ puts
41
+
42
+ pad_indexes = pendings.size.to_s.size
43
+ pendings.each_with_index do |pending, index|
44
+ puts " #{pad(index+1, pad_indexes)}) #{pending.test_name} (#{pending.pending_message})", :pending
45
+ puts indent("On line #{pending.line} of `#{pending.file}'", 6 + pad_indexes), :pending
46
+ puts
47
+ end
48
+ end
49
+
50
+ # Call on +:end+ to print a list of failures (failed assertions) and errors
51
+ # (unrescued exceptions), including file and line number where the test
52
+ # failed, and a short backtrace.
53
+ #
54
+ # It will not output anything if there weren't any pending tests.
55
+ #
56
+ # For example:
57
+ #
58
+ # on :end do |report|
59
+ # report.puts
60
+ # report.summarize_pending_tests
61
+ # end
62
+ #
63
+ # This relies on the public Report API, and on the presence of a #puts
64
+ # method to write to whatever source you are writing your report.
65
+ def summarize_errors
66
+ return if failures_and_errors.empty?
67
+
68
+ puts "Failures:"
69
+ puts
70
+
71
+ pad_indexes = failures_and_errors.size.to_s.size
72
+ failures_and_errors.each_with_index do |error, index|
73
+ colorize_as = ErroredTest === error ? :errored : :failed
74
+ puts " #{pad(index+1, pad_indexes)}) #{test_type(error)}: `#{error.test_name}' (on line #{error.line} of `#{error.file}')", colorize_as
75
+ # If error message has line breaks, indent the message
76
+ if error.error_message =~ /\n/
77
+ puts indent("with #{error.error.class}: <<", 6 + pad_indexes), colorize_as
78
+ puts indent(error.error_message, 6 + pad_indexes + 2), colorize_as
79
+ puts indent(">>", 6 + pad_indexes), colorize_as
80
+ else
81
+ puts indent("with #{error.error.class} `#{error.error_message}'", 6 + pad_indexes), colorize_as
82
+ end
83
+ indent(error.backtrace[0..2], 6 + pad_indexes).each {|backtrace| puts backtrace, colorize_as }
84
+ puts
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def running_time
91
+ "Ran in #{time_elapsed} seconds"
92
+ end
93
+
94
+ def test_totals
95
+ "%d test%s, %d assertion%s (%d passed, %d pending, %d failed, %d errored)" % [total_tests,
96
+ total_tests == 1 ? "" : "s",
97
+ assertions,
98
+ assertions == 1 ? "" : "s",
99
+ passes.size,
100
+ pendings.size,
101
+ failures.size,
102
+ errors.size]
103
+ end
104
+
105
+ def indent(strings, size=2, indent_with=" ")
106
+ # Don't use Array(strings) here in case strings contains line breaks. Here's why:
107
+ # <Array(strings)> is equivalent to <strings.to_a>
108
+ # <strings.to_a> is equivalent to <s = ""; strings.each {|x| s << x }; s>
109
+ # and in Ruby 1.8, String#each is equivalent to String#each_line
110
+ # so effectively it's equivalent to .split("\n") but keeping the \n's
111
+ strings = [strings] unless Array === strings
112
+ strings.map do |str|
113
+ str.to_s.split("\n").map {|s| indent_with * size + s }.join("\n")
114
+ end
115
+ end
116
+
117
+ def pad(str, amount)
118
+ " " * (amount - str.to_s.size) + str.to_s
119
+ end
120
+
121
+ def test_type(test)
122
+ case test # order is important since ErroredTest < FailedTest
123
+ when ErroredTest; "Error"
124
+ when FailedTest; "Failure"
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,6 @@
1
+ module Protest
2
+ # Utility mixins used in the framework, or available to Report authors to
3
+ # provide common functionality to the reports.
4
+ module Utils
5
+ end
6
+ end
data/lib/protest.rb ADDED
@@ -0,0 +1,108 @@
1
+ module Protest
2
+ VERSION = "0.2.4"
3
+
4
+ # Exception raised when an assertion fails. See TestCase#assert
5
+ class AssertionFailed < StandardError; end
6
+
7
+ # Exception raised to mark a test as pending. See TestCase#pending
8
+ class Pending < StandardError; end
9
+
10
+ # Register a new Report. This will make your report available to Protest,
11
+ # allowing you to run your tests through this report. For example
12
+ #
13
+ # module Protest
14
+ # class Reports::MyAwesomeReport < Report
15
+ # end
16
+ #
17
+ # add_report :awesomesauce, MyAwesomeReport
18
+ # end
19
+ #
20
+ # See Protest.report_with to see how to select which report will be used.
21
+ def self.add_report(name, report)
22
+ available_reports[name] = report
23
+ end
24
+
25
+ # Register a test case to be run with Protest. This is done automatically
26
+ # whenever you subclass Protest::TestCase, so you probably shouldn't pay
27
+ # much attention to this method.
28
+ def self.add_test_case(test_case)
29
+ available_test_cases << test_case
30
+ end
31
+
32
+ # Set to +false+ to avoid running tests +at_exit+. Default is +true+.
33
+ def self.autorun=(flag)
34
+ @autorun = flag
35
+ end
36
+
37
+ # Checks to see if tests should be run +at_exit+ or not. Default is +true+.
38
+ # See Protest.autorun=
39
+ def self.autorun?
40
+ !!@autorun
41
+ end
42
+
43
+ # Run all registered test cases through the selected report. You can pass
44
+ # arguments to the Report constructor here.
45
+ #
46
+ # See Protest.add_test_case and Protest.report_with
47
+ def self.run_all_tests!(*report_args)
48
+ Runner.new(@report).run(*available_test_cases)
49
+ end
50
+
51
+ # Select the name of the Report to use when running tests. See
52
+ # Protest.add_report for more information on registering a report.
53
+ #
54
+ # Any extra arguments will be forwarded to the report's #initialize method.
55
+ #
56
+ # The default report is Protest::Reports::Progress
57
+ def self.report_with(name, *report_args)
58
+ @report = report(name, *report_args)
59
+ end
60
+
61
+ # Load a report by name, initializing it with the extra arguments provided.
62
+ # If the given +name+ doesn't match a report registered via
63
+ # Protest.add_report then the method will raise IndexError.
64
+ def self.report(name, *report_args)
65
+ available_reports.fetch(name).new(*report_args)
66
+ end
67
+
68
+ # Set what object will filter the backtrace. It must respond to
69
+ # +filter_backtrace+, taking a backtrace array and a prefix path.
70
+ def self.backtrace_filter=(filter)
71
+ @backtrace_filter = filter
72
+ end
73
+
74
+ # The object that filters the backtrace
75
+ def self.backtrace_filter
76
+ @backtrace_filter
77
+ end
78
+
79
+ def self.available_test_cases
80
+ @test_cases ||= []
81
+ end
82
+ private_class_method :available_test_cases
83
+
84
+ def self.available_reports
85
+ @available_reports ||= {}
86
+ end
87
+ private_class_method :available_reports
88
+ end
89
+
90
+ require "protest/utils"
91
+ require "protest/utils/backtrace_filter"
92
+ require "protest/utils/summaries"
93
+ require "protest/utils/colorful_output"
94
+ require "protest/test_case"
95
+ require "protest/tests"
96
+ require "protest/runner"
97
+ require "protest/report"
98
+ require "protest/reports"
99
+ require "protest/reports/progress"
100
+ require "protest/reports/documentation"
101
+
102
+ Protest.autorun = true
103
+ Protest.report_with(:progress)
104
+ Protest.backtrace_filter = Protest::Utils::BacktraceFilter.new
105
+
106
+ at_exit do
107
+ Protest.run_all_tests! if Protest.autorun?
108
+ end