mcmire-protest 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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