riot 0.12.1 → 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +10 -0
  2. data/.yardopts +6 -0
  3. data/CHANGELOG +58 -46
  4. data/Gemfile +4 -0
  5. data/README.markdown +322 -85
  6. data/Rakefile +3 -38
  7. data/lib/riot.rb +74 -11
  8. data/lib/riot/assertion.rb +32 -1
  9. data/lib/riot/assertion_macro.rb +57 -10
  10. data/lib/riot/assertion_macros/any.rb +4 -2
  11. data/lib/riot/assertion_macros/assigns.rb +18 -4
  12. data/lib/riot/assertion_macros/empty.rb +2 -0
  13. data/lib/riot/assertion_macros/equals.rb +4 -0
  14. data/lib/riot/assertion_macros/equivalent_to.rb +5 -1
  15. data/lib/riot/assertion_macros/exists.rb +4 -2
  16. data/lib/riot/assertion_macros/includes.rb +5 -1
  17. data/lib/riot/assertion_macros/kind_of.rb +5 -1
  18. data/lib/riot/assertion_macros/matches.rb +5 -1
  19. data/lib/riot/assertion_macros/nil.rb +3 -1
  20. data/lib/riot/assertion_macros/not_borat.rb +6 -0
  21. data/lib/riot/assertion_macros/raises.rb +13 -7
  22. data/lib/riot/assertion_macros/respond_to.rb +5 -1
  23. data/lib/riot/assertion_macros/same_elements.rb +6 -2
  24. data/lib/riot/assertion_macros/size.rb +5 -1
  25. data/lib/riot/context.rb +58 -10
  26. data/lib/riot/context_helpers.rb +20 -4
  27. data/lib/riot/context_options.rb +14 -4
  28. data/lib/riot/message.rb +87 -6
  29. data/lib/riot/middleware.rb +69 -4
  30. data/lib/riot/reporter.rb +71 -110
  31. data/lib/riot/reporter/dot_matrix.rb +49 -0
  32. data/lib/riot/reporter/io.rb +85 -0
  33. data/lib/riot/reporter/pretty_dot_matrix.rb +38 -0
  34. data/lib/riot/reporter/silent.rb +18 -0
  35. data/lib/riot/reporter/story.rb +52 -0
  36. data/lib/riot/rr.rb +28 -4
  37. data/lib/riot/runnable.rb +53 -0
  38. data/lib/riot/situation.rb +45 -0
  39. data/lib/riot/version.rb +4 -0
  40. data/riot.gemspec +14 -155
  41. data/test/core/assertion_macros/any_test.rb +10 -10
  42. data/test/core/assertion_macros/assigns_test.rb +7 -7
  43. data/test/core/assertion_macros/equivalent_to_test.rb +3 -3
  44. data/test/core/assertion_macros/exists_test.rb +4 -4
  45. data/test/core/assertion_macros/includes_test.rb +2 -2
  46. data/test/core/assertion_macros/kind_of_test.rb +3 -3
  47. data/test/core/assertion_macros/matches_test.rb +2 -2
  48. data/test/core/assertion_macros/nil_test.rb +2 -2
  49. data/test/core/assertion_macros/raises_test.rb +10 -10
  50. data/test/core/assertion_macros/respond_to_test.rb +2 -2
  51. data/test/core/assertion_macros/same_elements_test.rb +4 -4
  52. data/test/core/assertion_macros/size_test.rb +6 -6
  53. data/test/core/context/asserts_with_arguments_test.rb +12 -0
  54. data/test/core/context/using_describe_in_a_test.rb +1 -1
  55. data/test/core/report_test.rb +9 -5
  56. data/test/core/runnable/message_test.rb +10 -6
  57. data/test/teststrap.rb +0 -6
  58. metadata +20 -33
  59. data/TODO.markdown +0 -14
  60. data/VERSION +0 -1
  61. data/test.watchr +0 -70
  62. data/test/benchmark/colorize.rb +0 -39
@@ -7,18 +7,28 @@ module Riot
7
7
  # context "Foo" do
8
8
  # set :transactional, true
9
9
  # end
10
- def set(key, value) options[key] = value; end
10
+ #
11
+ # @param [Object] key the key used to look up the option value later
12
+ # @param [Object] value the option value to store
13
+ def set(key, value)
14
+ options[key] = value
15
+ end
11
16
 
12
17
  # Returns the value of a set option. The key must match exactly, symbols and strings are not
13
18
  # interchangeable.
14
19
  #
20
+ # @param [Object] key the key used to look up the option value
15
21
  # @return [Object]
16
- def option(key) options[key]; end
22
+ def option(key)
23
+ options[key]
24
+ end
17
25
 
18
- # Returns the has of defined options.
26
+ # Returns the hash of defined options.
19
27
  #
20
28
  # @return [Hash]
21
- def options; @options ||= {}; end
29
+ def options
30
+ @options ||= {}
31
+ end
22
32
 
23
33
  end # ContextOptions
24
34
  end # Riot
@@ -3,21 +3,102 @@ class BlankSlate
3
3
  end
4
4
 
5
5
  module Riot
6
+ # A Message is similar in nature (but not implementation) to a string buffer; you put some strings in and
7
+ # calling {#to_s} will generate a single new string. What's special abnout Message is how you get strings
8
+ # into it. By convention, any method called on a Message that isn't defined will have its name turned into
9
+ # a string and any underscores replaced with spaces. This happens for each method call and those small
10
+ # messages are chained together at the end. For instance:
11
+ #
12
+ # message = Riot::Message.new
13
+ # message.hello_world.to_s
14
+ # => "hello world"
15
+ #
16
+ # message.whats_the_news.to_s
17
+ # => "hello world whats the news"
18
+ #
19
+ # For every method called it is also acceptable to pass any number of arguments. These arguments will be
20
+ # added to the final message after having {Kernel#inspect} called on them. Another for instance:
21
+ #
22
+ # message = Riot::Message.new
23
+ # message.expected([1, 2, 3], "foo").not([3, 2, 1], "bar")
24
+ # message.to_s
25
+ # => 'expected [1, 2, 3], "foo", not [3, 2, 1], "bar"'
26
+ #
27
+ # This is useful for - and was originally intended for - generating pass/fail messages from
28
+ # {Riot::AssertionMacro assertion macros}.
6
29
  class Message < BlankSlate
30
+
31
+ # Creates a new Message instance.
32
+ #
33
+ # @param [Array<Object>] *phrases an array of objects to be inspected
7
34
  def initialize(*phrases)
8
- @chunks = []; _inspect(phrases)
35
+ @chunks = []
36
+ _inspect(phrases)
9
37
  end
10
38
 
39
+ # Generates the string value of the built-up message.
40
+ #
41
+ # @return [String]
11
42
  def to_s; @chunks.join.strip; end
43
+ alias_method :inspect, :to_s
44
+
45
+ # Converts any method call into a more readable string by replacing underscores with spaces. Any
46
+ # arguments to the method are inspected and appended to the final message. Blocks are currently ignored.
47
+ #
48
+ # @param [String, Symbol] meth the method name to be converted into a more readable form
49
+ # @param [Array<Object>] *phrases an array of objects to be inspected
50
+ # @return [Riot::Message] this instance for use in chaining calls
51
+ def method_missing(meth, *phrases, &block)
52
+ push(meth.to_s.gsub('_', ' '))
53
+ _inspect(phrases)
54
+ end
12
55
 
13
- def method_missing(meth, *phrases, &blk) push(meth.to_s.gsub('_', ' ')); _inspect(phrases); end
56
+ # Adds a comma then the provided phrase to this message.
57
+ #
58
+ # Riot::Message.new.hello.comma("world").to_s
59
+ # => "hello, world"
60
+ #
61
+ # @param [String] str any string phrase to be added after the comma
62
+ # @param [Array<Object>] *phrases an array of objects to be inspected
63
+ # @return [Riot::Message] this instance for use in chaining calls
64
+ def comma(str, *phrases)
65
+ _concat([", ", str])
66
+ _inspect(phrases)
67
+ end
14
68
 
15
- def comma(str, *phrases) _concat([", ", str]); _inspect(phrases); end
69
+ # Adds the string ", but".
70
+ #
71
+ # Riot::Message.new.any_number.but(52).to_s
72
+ # => "any number, but 52"
73
+ #
74
+ # @param [Array<Object>] *phrases an array of objects to be inspected
75
+ # @return [Riot::Message] this instance for use in chaining calls
16
76
  def but(*phrases); comma("but", *phrases); end
77
+
78
+ # Adds the string ", not".
79
+ #
80
+ # Riot::Message.new.expected_freebies.not("$1.50").to_s
81
+ # => 'expected freebies, not "$1.50"'
82
+ #
83
+ # @param [Array<Object>] *phrases an array of objects to be inspected
84
+ # @return [Riot::Message] this instance for use in chaining calls
17
85
  def not(*phrases); comma("not", *phrases); end
18
- def push(str) _concat([" ", str]); end
86
+
19
87
  private
20
- def _concat(chunks) @chunks.concat(chunks); self; end
21
- def _inspect(phrases) phrases.each { |phrase| push(phrase.inspect) }; self; end
88
+ def push(str)
89
+ _concat([" ", str])
90
+ end
91
+
92
+ def _concat(chunks)
93
+ @chunks.concat(chunks)
94
+ self
95
+ end
96
+
97
+ def _inspect(phrases)
98
+ unless phrases.empty?
99
+ push(phrases.map { |phrase| phrase.inspect }.join(", "))
100
+ end
101
+ self
102
+ end
22
103
  end # Message
23
104
  end # Riot
@@ -1,5 +1,60 @@
1
1
  module Riot
2
2
 
3
+ # Context middlewares are chainable, context preparers. This to say that a middleware knows about a single
4
+ # neighbor and that it can prepare context before the context is "run". As a for instance, suppose you
5
+ # wanted the following to be possible.
6
+ #
7
+ # context Person do
8
+ # denies(:valid?)
9
+ # end # Person
10
+ #
11
+ # Without writing a middleware, the topic in this would actually be nil, but what the context is saying is
12
+ # that there should be something in the topic that responds to +:valid?+; an instance of +Person+ in this
13
+ # case. We can do this with middleware like so:
14
+ #
15
+ # class Modelware < Riot::ContextMiddleware
16
+ # register
17
+ #
18
+ # def call(context)
19
+ # if context.description.kind_of?(Model)
20
+ # context.setup { context.description.new }
21
+ # end
22
+ # middleware.call(context)
23
+ # end
24
+ # end # Modelware
25
+ #
26
+ # That's good stuff. If you're familiar at all with the nature of Rack middleware - how to implement it,
27
+ # how it's executed, etc. - you'll be familiar with Context middleware as the principles are similar:
28
+ #
29
+ # 1. Define a class that extends {Riot::ContextMiddleware}
30
+ # 2. Call +register+
31
+ # 3. Implement a +call+ method that accepts the Context that is about to be executed
32
+ # 4. Do stuff, but make sure to pass the call along with +middleware.call(context)+
33
+ #
34
+ # Steps 1, 2, and 3 should be pretty straight-forward. Currently, +context+ is the only argument to +call+.
35
+ # When your middleware is initialized it is given the next registered middleware in the chain (which is
36
+ # where the `middleware` method gets its value from).
37
+ #
38
+ # So, "Do stuff" from step 4 is the where we start breaking things down. What can you actually do? Well,
39
+ # you can do anything to the context that you could do if you were writing a Riot test; and I do mean
40
+ # anything.
41
+ #
42
+ # * Add setup blocks (as many as you like)
43
+ # * Add teardown blocks (as many as you like)
44
+ # * Add hookup blocks (as many as you like)
45
+ # * Add helpers (as many as you like)
46
+ # * Add assertions
47
+ #
48
+ # The context in question will not run before all middleware have been applied to the context; this is
49
+ # different behavior than that of Rack middleware. {Riot::ContextMiddleware} is only about preparing a
50
+ # context, not about executing it. Thus, where in your method you actually pass the call off to the next
51
+ # middleware in the chain has impact on how the context is set up. Basically, whatever you do before
52
+ # calling `middleware.call(context)` is done before any other middleware gets setup and before the innards
53
+ # of the context itself are applied. Whatever you do after that call is done after all that, but still
54
+ # before the actual setups, hookups, assertions, and teardowns are run.
55
+ #
56
+ # Do not expect the same instance of middleware to exist from one {Riot::Context} instance to the next. It
57
+ # is highly likely that each {Riot::Context} will instantiate their own middleware instances.
3
58
  class ContextMiddleware
4
59
  # Registers the current middleware class with Riot so that it may be included in the set of middlewares
5
60
  # Riot will poke before executing a Context.
@@ -12,16 +67,25 @@ module Riot
12
67
  # context.hookup { ... }
13
68
  # end
14
69
  # end
15
- def self.register; Context.middlewares << self; end
70
+ def self.register
71
+ Context.middlewares << self
72
+ end
16
73
 
17
- attr_reader :middleware # Theoretically, the next middleware in the stack
74
+ # Theoretically, the next middleware in the stack
75
+ attr_reader :middleware
18
76
 
77
+ # Create a new middleware instance and give it the next middleware in the chain.
78
+ #
79
+ # @param [Riot::ContextMiddleware] middleware the next middleware instance in the chain
19
80
  def initialize(middleware)
20
81
  @middleware = middleware
21
82
  end
22
83
 
23
- # The meat. Because you have access to the Context, you can add your own setups,
24
- # hookups, etc. +call+ will be called before any tests are run, but after the Context is configured.
84
+ # The magic happens here. Because you have access to the Context, you can add your own setups, hookups,
85
+ # etc. +call+ will be called before any tests are run, but after the Context is configured. Though
86
+ # something will likely be returned, do not put any faith in what that will be.
87
+ #
88
+ # @param [Riot::Context] context the Context instance that will be prepared by registered middleware
25
89
  def call(context)
26
90
  raise "You should implement call yourself"
27
91
  end
@@ -34,6 +98,7 @@ module Riot
34
98
  @context_definition = context_definition
35
99
  end
36
100
 
101
+ # (see Riot::ContextMiddleware#call)
37
102
  def call(context) context.instance_eval(&@context_definition); end
38
103
  end # AllImportantMiddleware
39
104
 
@@ -1,7 +1,27 @@
1
1
  module Riot
2
+
3
+ # A Reporter decides how to output the result of a test. When a context is set to be executed, the
4
+ # {Riot::Reporter#describe_context} method is called with the context that will be running; this remains
5
+ # so until the next context is executed. After each {Riot#Assertion#evaluate assertion is evaluated},
6
+ # {Riot::Reporter#report} is called with the description of the assertion and the resulting response.
7
+ #
8
+ # The general idea is that a sub-class of Reporter should be defined that knows specifically how to
9
+ # output the reported results. In the sub-class, you simply need to implement a +pass+, +fail+, +error+,
10
+ # and +results+ method.
2
11
  class Reporter
3
- attr_accessor :passes, :failures, :errors, :current_context
12
+ # Count of successful assertions so far
13
+ attr_accessor :passes
14
+
15
+ # Count of failed assertions so far
16
+ attr_accessor :failures
17
+
18
+ # Count of errored assertions so far
19
+ attr_accessor :errors
4
20
 
21
+ # The context that is currently being reported on
22
+ attr_accessor :current_context
23
+
24
+ # Creates a new Reporter instance and initializes counts to zero
5
25
  def initialize
6
26
  @passes = @failures = @errors = 0
7
27
  @current_context = ""
@@ -9,8 +29,17 @@ module Riot
9
29
 
10
30
  def new(*args, &block); self; end
11
31
 
12
- def success?; (@failures + @errors) == 0; end
32
+ # Returns true if no failures or errors have been produced yet.
33
+ #
34
+ # @return [Boolean]
35
+ def success?
36
+ (@failures + @errors) == 0
37
+ end
13
38
 
39
+ # Starts a timer, execute the provided block, then reports the results. Useful for timing context
40
+ # execution(s).
41
+ #
42
+ # @param [lambda] &block the contexts to run
14
43
  def summarize(&block)
15
44
  started = Time.now
16
45
  yield
@@ -18,8 +47,18 @@ module Riot
18
47
  results(Time.now - started)
19
48
  end
20
49
 
21
- def describe_context(context); @current_context = context; end
50
+ # Called when a new context is about to execute to set the state for this Reporter instance.
51
+ #
52
+ # @param [Riot::Context] context the context that is about to execute
53
+ def describe_context(context)
54
+ @current_context = context
55
+ end
22
56
 
57
+ # Called immediately after an assertion has been evaluated. From this method either +pass+, +fail+,
58
+ # or +error+ will be called.
59
+ #
60
+ # @param [String] description the description of the assertion
61
+ # @param [Array<Symbol, *[Object]>] response the evaluation response from the assertion
23
62
  def report(description, response)
24
63
  code, result = *response
25
64
  case code
@@ -36,120 +75,42 @@ module Riot
36
75
  end
37
76
  end
38
77
 
39
- def pass(description, result); end
40
- def fail(description, message, line, file); end
41
- def error(description, result); end
42
- end # Reporter
43
-
44
- class IOReporter < Reporter
45
- def initialize(writer=STDOUT)
46
- super()
47
- @writer = writer
48
- end
49
- def puts(message) @writer.puts(message); end
50
- def print(message) @writer.print(message); end
51
-
52
- def line_info(line, file)
53
- line ? "(on line #{line} in #{file})" : ""
54
- end
55
-
56
- def results(time_taken)
57
- values = [passes, failures, errors, ("%0.6f" % time_taken)]
58
- puts "\n%d passes, %d failures, %d errors in %s seconds" % values
59
- end
60
-
61
- def format_error(e)
62
- format = []
63
- format << " #{e.class.name} occurred"
64
- format << "#{e.to_s}"
65
- filter_backtrace(e.backtrace) { |line| format << " at #{line}" }
66
-
67
- format.join("\n")
68
- end
69
-
70
- protected
71
- def filter_backtrace(backtrace)
72
- cleansed = []
73
- bad = true
74
-
75
- # goal is to filter all the riot stuff/rake before the first non riot thing
76
- backtrace.reverse_each do |bt|
77
- # make sure we are still in the bad part
78
- bad = (bt =~ /\/lib\/riot/ || bt =~ /rake_test_loader/) if bad
79
-
80
- yield bt unless bad
81
- end
82
-
83
- cleansed.empty?? backtrace : cleansed
84
- end
85
-
86
- begin
87
- raise LoadError if ENV["TM_MODE"]
88
- require 'rubygems'
89
- require 'term/ansicolor'
90
- include Term::ANSIColor
91
- rescue LoadError
92
- def green(str); str; end
93
- alias :red :green
94
- alias :yellow :green
78
+ # Called if the assertion passed.
79
+ #
80
+ # @param [String] description the description of the assertion
81
+ # @param [Array<Symbol, String]>] result the evaluation response from the assertion
82
+ def pass(description, result)
83
+ raise "Implement this in a sub-class"
95
84
  end
96
- end
97
-
98
- class StoryReporter < IOReporter
99
- def describe_context(context)
100
- super
101
- puts context.detailed_description
102
- end
103
- def pass(description, message) puts " + " + green("#{description} #{message}".strip); end
104
85
 
86
+ # Called if the assertion failed.
87
+ #
88
+ # @param [String] description the description of the assertion
89
+ # @param [Array<Symbol, String, Number, String]>] response the evaluation response from the assertion
105
90
  def fail(description, message, line, file)
106
- puts " - " + yellow("#{description}: #{message} #{line_info(line, file)}".strip)
91
+ raise "Implement this in a sub-class"
107
92
  end
108
93
 
109
- def error(description, e) puts " ! " + red("#{description}: #{e.message}"); end
110
- end
111
-
112
- class VerboseStoryReporter < StoryReporter
113
- def error(description, e)
114
- super
115
- puts red(format_error(e))
116
- end
117
- end
118
-
119
- class DotMatrixReporter < IOReporter
120
- def initialize(writer=STDOUT)
121
- super
122
- @details = []
123
- end
124
-
125
- def pass(description, message)
126
- print green(".")
127
- end
128
-
129
- def fail(description, message, line, file)
130
- print yellow("F")
131
- @details << "FAILURE - #{test_detail(description, message)} #{line_info(line, file)}".strip
132
- end
133
-
134
- def error(description, e)
135
- print red("E")
136
- @details << "ERROR - #{test_detail(description, format_error(e))}"
94
+ # Called if the assertion had an unexpected error.
95
+ #
96
+ # @param [String] description the description of the assertion
97
+ # @param [Array<Symbol, Exception]>] result the exception from the assertion
98
+ def error(description, result)
99
+ raise "Implement this in a sub-class"
137
100
  end
138
101
 
102
+ # Called after all contexts have finished. This is where the final results can be output.
103
+ #
104
+ # @param [Number] time_taken number of seconds taken to run everything
139
105
  def results(time_taken)
140
- puts "\n#{@details.join("\n\n")}" unless @details.empty?
141
- super
142
- end
143
- private
144
- def test_detail(description, message)
145
- "#{current_context.detailed_description} #{description} => #{message}"
106
+ raise "Implement this in a sub-class"
146
107
  end
147
- end
148
-
149
- class SilentReporter < Reporter
150
- def pass(description, message); end
151
- def fail(description, message, line, file); end
152
- def error(description, e); end
153
- def results(time_taken); end
154
- end
108
+ end # Reporter
109
+
155
110
  end # Riot
111
+
112
+ require 'riot/reporter/silent'
113
+ require 'riot/reporter/io'
114
+ require 'riot/reporter/story'
115
+ require 'riot/reporter/dot_matrix'
116
+ require 'riot/reporter/pretty_dot_matrix'