spinach 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gitignore +1 -0
  2. data/.yardopts +6 -0
  3. data/Gemfile +1 -0
  4. data/Guardfile +7 -0
  5. data/Rakefile +1 -0
  6. data/Readme.md +147 -10
  7. data/features/{generate_features.feature → automatic_feature_generation.feature} +0 -0
  8. data/features/steps/automatic_feature_generation.rb +5 -2
  9. data/features/steps/exit_status.rb +8 -3
  10. data/features/steps/feature_name_guessing.rb +4 -1
  11. data/features/steps/reporting/display_run_summary.rb +7 -4
  12. data/features/steps/reporting/error_reporting.rb +7 -2
  13. data/features/steps/reporting/show_step_source_location.rb +9 -5
  14. data/features/steps/reporting/undefined_feature_reporting.rb +4 -1
  15. data/features/steps/rspec_compatibility.rb +6 -2
  16. data/features/support/spinach_runner.rb +2 -9
  17. data/lib/spinach/capybara.rb +3 -4
  18. data/lib/spinach/cli.rb +0 -1
  19. data/lib/spinach/config.rb +0 -8
  20. data/lib/spinach/dsl.rb +4 -13
  21. data/lib/spinach/exceptions.rb +1 -1
  22. data/lib/spinach/feature_steps.rb +1 -9
  23. data/lib/spinach/generators/feature_generator.rb +3 -2
  24. data/lib/spinach/generators.rb +1 -1
  25. data/lib/spinach/hookable.rb +81 -0
  26. data/lib/spinach/hooks.rb +132 -0
  27. data/lib/spinach/reporter/stdout/error_reporting.rb +163 -0
  28. data/lib/spinach/reporter/stdout.rb +3 -151
  29. data/lib/spinach/reporter.rb +39 -25
  30. data/lib/spinach/runner/{feature.rb → feature_runner.rb} +5 -15
  31. data/lib/spinach/runner/scenario_runner.rb +65 -0
  32. data/lib/spinach/runner.rb +5 -11
  33. data/lib/spinach/version.rb +1 -1
  34. data/lib/spinach.rb +20 -8
  35. data/spinach.gemspec +2 -3
  36. data/test/spinach/capybara_test.rb +4 -3
  37. data/test/spinach/cli_test.rb +0 -1
  38. data/test/spinach/feature_steps_test.rb +6 -23
  39. data/test/spinach/generators/feature_generator_test.rb +2 -2
  40. data/test/spinach/generators_test.rb +2 -2
  41. data/test/spinach/hookable_test.rb +59 -0
  42. data/test/spinach/hooks_test.rb +28 -0
  43. data/test/spinach/reporter/stdout/error_reporting_test.rb +265 -0
  44. data/test/spinach/reporter/stdout_test.rb +1 -238
  45. data/test/spinach/reporter_test.rb +58 -103
  46. data/test/spinach/runner/{feature_test.rb → feature_runner_test.rb} +21 -23
  47. data/test/spinach/runner/scenario_runner_test.rb +111 -0
  48. data/test/spinach/runner_test.rb +1 -1
  49. data/test/spinach_test.rb +19 -18
  50. data/test/test_helper.rb +1 -1
  51. metadata +60 -61
  52. data/lib/spinach/runner/scenario.rb +0 -77
  53. data/test/spinach/runner/scenario_test.rb +0 -120
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  *.rbc
3
+ .rbx
3
4
  .bundle
4
5
  .config
5
6
  .yardoc
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --title "Spinach"
2
+ --no-private
3
+ -m markdown
4
+ -r Readme.md
5
+ lib/**/*.rb
6
+ Readme.md
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gemspec
6
6
  group :test do
7
7
  gem 'guard'
8
8
  gem 'guard-minitest'
9
+ gem 'guard-spinach'
9
10
  end
10
11
 
11
12
  group :darwin do
data/Guardfile CHANGED
@@ -3,3 +3,10 @@ guard 'minitest' do
3
3
  watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
4
4
  watch(%r|^test/test_helper\.rb|) { "test" }
5
5
  end
6
+
7
+ guard 'spinach' do
8
+ watch(%r|^features/(.*)\.feature|)
9
+ watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m|
10
+ "features/#{m[1]}#{m[2]}.feature"
11
+ end
12
+ end
data/Rakefile CHANGED
@@ -9,6 +9,7 @@ Rake::TestTask.new do |t|
9
9
  # t.loader = :direct
10
10
  end
11
11
 
12
+ desc 'Run spinach features'
12
13
  task :spinach do
13
14
  exec "bundle exec spinach"
14
15
  end
data/Readme.md CHANGED
@@ -1,15 +1,152 @@
1
- # About Spinach
2
- Spinach is a BDD framework on top of gherkin
1
+ # Spinach - BDD framework on top of Gherkin [![Build Status](https://secure.travis-ci.org/codegram/spinach.png)](http://travis-ci.org/codegram/spinach)
3
2
 
4
- ![](http://farm1.static.flickr.com/58/200481513_a1a0aa265a.jpg)
3
+ Spinach is a high-level BDD framework that leverages the expressive
4
+ [Gherkin language][gherkin] (used by [Cucumber][cucumber]) to help you define
5
+ executable specifications of your application or library's acceptance criteria.
5
6
 
6
- # Documentation
7
- [Spinach documentation at rubydoc.info](http://rubydoc.info/github/codegram/spinach/master/frames)
7
+ Conceived as an alternative to Cucumber, here are some of its design goals:
8
8
 
9
- # Testimonials
10
- ![](http://www.80stees.com/images/products/Popeye_the_Sailor_Man_I_Popeye_Spinach-T-link.jpg)
9
+ * Step maintanability: since features map to their own classes, their steps are
10
+ just methods of that class. This encourages step encapsulation.
11
11
 
12
- *Popeye the Sailor*
12
+ * Step reusability: In case you want to reuse steps across features, you can
13
+ always wrap those in plain ol' Ruby modules.
13
14
 
14
- # Build status
15
- [![Build Status](https://secure.travis-ci.org/codegram/spinach.png)](http://travis-ci.org/codegram/spinach)
15
+ Spinach is tested against MRI 1.9.2, 1.9.3. Rubinius 2.0 support is on the
16
+ works.
17
+
18
+ We are not planning to make it compatible with MRI 1.8.7 since, you know, this
19
+ would be irresponsible :)
20
+
21
+ ## Getting started
22
+
23
+ Start by adding spinach to your Gemfile:
24
+
25
+ group :test do
26
+ gem 'spinach'
27
+ # along with gem 'minitest' or gem 'rspec'
28
+ end
29
+
30
+ Spinach works with your favorite test suite, you just have to tell it which
31
+ one are you going to use in `features/support/env.rb`:
32
+
33
+ # If you want to use minitest:
34
+ require 'minitest/spec'
35
+
36
+ # If you want to use rspec:
37
+ require 'rspec'
38
+
39
+ Now create a `features` folder in your app or library and write your first
40
+ feature:
41
+
42
+ ## features/test_how_spinach_works.feature
43
+
44
+ Feature: Test how spinach works
45
+ In order to know what the heck is spinach
46
+ As a developer
47
+ I want it to behave in an expected way
48
+
49
+ Scenario: Formal greeting
50
+ Given I have an empty array
51
+ And I append my first name and my last name to it
52
+ When I pass it to my super-duper method
53
+ Then the output should contain a formal greeting
54
+
55
+ Scenario: Informal greeting
56
+ Given I have an empty array
57
+ And I append only my first name to it
58
+ When I pass it to my super-duper method
59
+ Then the output should contain a casual greeting
60
+
61
+ Now for the steps file. Remember that in Spinach steps are just Ruby classes,
62
+ following a camelcase naming convention. Spinach generator will do some
63
+ scaffolding for you:
64
+
65
+ $ spinach --generate
66
+
67
+ Spinach will detect your features and generate the following class:
68
+
69
+ ## features/steps/test_how_spinach_works.rb
70
+
71
+ class TestHowSpinachWorks < Spinach::FeatureSteps
72
+ Given 'I have an empty array' do
73
+ end
74
+
75
+ And 'I append my first name and my last name to it' do
76
+ end
77
+
78
+ When 'I pass it to my super-duper method' do
79
+ end
80
+
81
+ Then 'the output should contain a formal greeting' do
82
+ end
83
+
84
+ And 'I append only my first name to it' do
85
+ end
86
+
87
+ Then 'the output should contain a casual greeting' do
88
+ end
89
+ end
90
+
91
+ Then, you can fill it in with your logic - remember, it's just a class, you can
92
+ use private methods, mix in modules or whatever!
93
+
94
+ class TestHowSpinachWorks < Spinach::FeatureSteps
95
+ Given 'I have an empty array' do
96
+ @array = Array.new
97
+ end
98
+
99
+ And 'I append my first name and my last name to it' do
100
+ @array += ["John", "Doe"]
101
+ end
102
+
103
+ When 'I pass it to my super-duper method' do
104
+ @output = capture_output do
105
+ Greeter.greet(@array)
106
+ end
107
+ end
108
+
109
+ Then 'the output should contain a formal salutation' do
110
+ @output.must_include "Hello, mr. John Doe"
111
+ end
112
+
113
+ And 'I append only my first name to it' do
114
+ @array += ["John"]
115
+ end
116
+
117
+ Then 'the output should contain a casual salutation' do
118
+ @output.must_include "Yo, John! Whassup?"
119
+ end
120
+
121
+ private
122
+
123
+ def capture_output
124
+ out = StreamIO.new
125
+ $stdout = out
126
+ $stderr = out
127
+ yield
128
+ $stdout = STDOUT
129
+ $stderr = STDERR
130
+ out.string
131
+ end
132
+ end
133
+
134
+ Then run your feature again running `spinach` and watch it all turn green! :)
135
+
136
+ ## Contributing
137
+
138
+ You can easily contribute to Spinach. Its codebase is simple and
139
+ [extensively documented][documentation].
140
+
141
+ * Fork the project.
142
+ * Make your feature addition or bug fix.
143
+ * Add specs for it. This is important so we don't break it in a future
144
+ version unintentionally.
145
+ * Commit, do not mess with rakefile, version, or history.
146
+ If you want to have your own version, that is fine but bump version
147
+ in a commit by itself I can ignore when I pull.
148
+ * Send me a pull request. Bonus points for topic branches.
149
+
150
+ [gherkin]: http://github.com/cucumber/gherkin
151
+ [cucumber]: http://github.com/cucumber/cucumber
152
+ [documentation]: http://rubydoc.info/github/codegram/spinach/master/frames
@@ -1,4 +1,7 @@
1
- Feature 'Automatic feature generation' do
1
+ class AutomaticFeatureGeneration < Spinach::FeatureSteps
2
+
3
+ feature 'Automatic feature generation'
4
+
2
5
  include Integration::SpinachRunner
3
6
  Given 'I have defined a "Cheezburger can I has" feature' do
4
7
  write_file('features/cheezburger_can_i_has.feature',
@@ -20,7 +23,7 @@ Feature 'Automatic feature generation' do
20
23
  File.exists?(@file).must_equal true
21
24
  end
22
25
  end
23
-
26
+
24
27
  And "that feature should have the example feature steps" do
25
28
  in_current_dir do
26
29
  content = File.read(@file)
@@ -1,4 +1,7 @@
1
- Feature "Exit status" do
1
+ class ExitStatus < Spinach::FeatureSteps
2
+
3
+ feature "Exit status"
4
+
2
5
  include Integration::SpinachRunner
3
6
 
4
7
  Given "I have a feature that has no error or failure" do
@@ -9,7 +12,8 @@ Feature "Exit status" do
9
12
  Then I succeed
10
13
  ')
11
14
  write_file('features/steps/success_feature.rb',
12
- 'Feature "A success feature" do
15
+ 'class ASuccessFeature < Spinach::FeatureSteps
16
+ feature "A success feature"
13
17
  Then "I succeed" do
14
18
  end
15
19
  end')
@@ -24,7 +28,8 @@ Feature "Exit status" do
24
28
  Then I fail
25
29
  ')
26
30
  write_file('features/steps/failure_feature.rb',
27
- 'Feature "A failure feature" do
31
+ 'class AFailureFeature < Spinach::FeatureSteps
32
+ feature "A failure feature"
28
33
  Then "I fail" do
29
34
  true.must_equal false
30
35
  end
@@ -1,4 +1,7 @@
1
- Feature "Feature name guessing" do
1
+ class FeatureNameGuessing < Spinach::FeatureSteps
2
+
3
+ feature "Feature name guessing"
4
+
2
5
  include Integration::SpinachRunner
3
6
 
4
7
  Given 'I am writing a feature called "My cool feature"' do
@@ -1,6 +1,9 @@
1
1
  require 'aruba/api'
2
2
 
3
- Feature "Display run summary" do
3
+ class DisplayRunSummary < Spinach::FeatureSteps
4
+
5
+ feature 'automatic'
6
+
4
7
  include Integration::SpinachRunner
5
8
 
6
9
  Given "I have a feature that has some successful, undefined, failed and error steps" do
@@ -24,7 +27,9 @@ Feature "Display run summary" do
24
27
  Then I must succeed
25
28
  ')
26
29
  write_file('features/steps/test_feature.rb',
27
- 'Feature "A test feature" do
30
+ 'class ATestFeature < Spinach::FeatureSteps
31
+ feature "A test feature"
32
+
28
33
  Given "I am a fool" do
29
34
  end
30
35
 
@@ -65,5 +70,3 @@ Feature "Display run summary" do
65
70
  )
66
71
  end
67
72
  end
68
-
69
-
@@ -1,4 +1,7 @@
1
- Feature "Error reporting" do
1
+ class ErrorReporting < Spinach::FeatureSteps
2
+
3
+ feature "Error reporting"
4
+
2
5
  include Integration::SpinachRunner
3
6
  include Integration::ErrorReporting
4
7
 
@@ -12,7 +15,9 @@ Feature "Error reporting" do
12
15
  ')
13
16
 
14
17
  write_file('features/steps/failure_feature.rb',
15
- 'Feature "Feature with failures" do
18
+ 'class FeatureWithFailures < Spinach::FeatureSteps
19
+ feature "Feature with failures"
20
+
16
21
  Given "true is false" do
17
22
  true.must_equal false
18
23
  end
@@ -1,6 +1,9 @@
1
1
  require 'aruba/api'
2
2
 
3
- Feature "Show step source location" do
3
+ class ShowStepSourceLocation < Spinach::FeatureSteps
4
+
5
+ feature "Show step source location"
6
+
4
7
  include Integration::SpinachRunner
5
8
 
6
9
  Given "I have a feature that has no error or failure" do
@@ -11,7 +14,8 @@ Feature "Show step source location" do
11
14
  Then I succeed
12
15
  ')
13
16
  write_file('features/steps/success_feature.rb',
14
- 'Feature "A success feature" do
17
+ 'class ASuccessFeature < Spinach::FeatureSteps
18
+ feature "A success feature"
15
19
  Then "I succeed" do
16
20
  end
17
21
  end')
@@ -24,7 +28,7 @@ Feature "Show step source location" do
24
28
 
25
29
  Then "I should see the source location of each step of every scenario" do
26
30
  all_stdout.must_match(
27
- /I succeed.*features\/steps\/success_feature\.rb.*2/
31
+ /I succeed.*features\/steps\/success_feature\.rb.*3/
28
32
  )
29
33
  end
30
34
 
@@ -36,7 +40,8 @@ Feature "Show step source location" do
36
40
  Given this is a external step
37
41
  ')
38
42
  write_file('features/steps/success_feature.rb',
39
- 'Feature "A feature that uses external steps" do
43
+ 'class AFeatureThatUsesExternalSteps < Spinach::FeatureSteps
44
+ feature "A feature that uses external steps"
40
45
  include ExternalSteps
41
46
  end')
42
47
  write_file('features/support/external_steps.rb',
@@ -54,4 +59,3 @@ Feature "Show step source location" do
54
59
  )
55
60
  end
56
61
  end
57
-
@@ -1,4 +1,7 @@
1
- Feature "Undefined feature reporting" do
1
+ class UndefinedFeatureReporting < Spinach::FeatureSteps
2
+
3
+ feature "Undefined feature reporting"
4
+
2
5
  include Integration::SpinachRunner
3
6
 
4
7
  Given "I've written a feature but not its steps" do
@@ -1,4 +1,7 @@
1
- Feature "RSpec compatibility" do
1
+ class RSpecCompatibility < Spinach::FeatureSteps
2
+
3
+ feature "RSpec compatibility"
4
+
2
5
  include Integration::SpinachRunner
3
6
  include Integration::ErrorReporting
4
7
 
@@ -12,7 +15,8 @@ Feature "RSpec compatibility" do
12
15
  ')
13
16
 
14
17
  write_file('features/steps/failure_feature.rb',
15
- 'Feature "Feature with failures" do
18
+ 'class FeatureWithFailures < Spinach::FeatureSteps
19
+ feature "Feature with failures"
16
20
  Given "true is false" do
17
21
  true.should == false
18
22
  end
@@ -5,15 +5,8 @@ module Integration
5
5
  include Aruba::Api
6
6
 
7
7
  def self.included(base)
8
- base.class_eval do
9
- before_scenario do
10
- in_current_dir do
11
- FileUtils.rm_rf("features")
12
- end
13
- end
14
- before_scenario do
15
- @aruba_timeout_seconds = 6
16
- end
8
+ Spinach.hooks.before_scenario do
9
+ @aruba_timeout_seconds = 6
17
10
  end
18
11
  end
19
12
 
@@ -24,10 +24,9 @@ module Spinach
24
24
  def self.included(base)
25
25
  base.class_eval do
26
26
  include ::Capybara::DSL
27
-
28
- after_scenario do
29
- ::Capybara.current_session.reset! if ::Capybara.app
30
- end
27
+ end
28
+ Spinach.hooks.before_scenario do
29
+ ::Capybara.current_session.reset! if ::Capybara.app
31
30
  end
32
31
  end
33
32
  end
data/lib/spinach/cli.rb CHANGED
@@ -35,7 +35,6 @@ module Spinach
35
35
  def init_reporter
36
36
  reporter =
37
37
  Spinach::Reporter::Stdout.new(options[:reporter])
38
- Spinach.config.default_reporter = reporter
39
38
  reporter.bind
40
39
  end
41
40
 
@@ -45,14 +45,6 @@ module Spinach
45
45
  @support_path || 'features/support'
46
46
  end
47
47
 
48
- # The default reporter is the reporter spinach will use if there's no other
49
- # specified. Defaults to Spinach::Reporter::Stdout, which will print all
50
- # output to the standard output
51
- #
52
- def default_reporter
53
- @default_reporter || Spinach::Reporter::Stdout.new
54
- end
55
-
56
48
  # Allows you to read the config object using a hash-like syntax.
57
49
  #
58
50
  # @param [String] attribute
data/lib/spinach/dsl.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'hooks'
2
-
3
1
  module Spinach
4
2
  # Spinach DSL aims to provide an easy way to define steps and hooks into your
5
3
  # feature classes.
@@ -13,13 +11,6 @@ module Spinach
13
11
  base.class_eval do
14
12
  include InstanceMethods
15
13
  extend ClassMethods
16
- include Hooks
17
-
18
- define_hook :before_scenario
19
- define_hook :after_scenario
20
- define_hook :before_step
21
- define_hook :after_step
22
-
23
14
  end
24
15
  end
25
16
 
@@ -82,11 +73,11 @@ module Spinach
82
73
  #
83
74
  # @api public
84
75
  def execute_step(step)
85
- undercored_step = Spinach::Support.underscore(step)
76
+ underscored_step = Spinach::Support.underscore(step)
86
77
  location = nil
87
- if self.respond_to?(undercored_step)
88
- location = method(undercored_step).source_location
89
- self.send(undercored_step)
78
+ if self.respond_to?(underscored_step)
79
+ location = method(underscored_step).source_location
80
+ self.send(underscored_step)
90
81
  else
91
82
  raise Spinach::StepNotDefinedException.new(step)
92
83
  end
@@ -23,7 +23,7 @@ module Spinach
23
23
  end
24
24
 
25
25
  # This class represents the exception raised when Spinach can't find a step
26
- # for a {Scenario}.
26
+ # for a {FeatureSteps}.
27
27
  #
28
28
  class StepNotDefinedException < StandardError
29
29
  attr_reader :feature, :step
@@ -11,15 +11,7 @@ module Spinach
11
11
  #
12
12
  # @api public
13
13
  def self.inherited(base)
14
- Spinach.features << base
14
+ Spinach.feature_steps << base
15
15
  end
16
16
  end
17
17
  end
18
-
19
- # Syntactic sugar. Define the "Feature do" syntax.
20
- Object.send(:define_method, :Feature) do |name, &block|
21
- Class.new(Spinach::FeatureSteps) do
22
- feature name
23
- class_eval &block
24
- end
25
- end
@@ -1,6 +1,6 @@
1
1
  module Spinach
2
2
  module Generators
3
- # A feature generator generates and/or writes an example feature steps class
3
+ # A feature generator generates and/or writes an example feature steps class
4
4
  # given the parsed feture data
5
5
  class FeatureGenerator
6
6
 
@@ -43,7 +43,8 @@ module Spinach
43
43
  # an example feature steps definition
44
44
  def generate
45
45
  result = StringIO.new
46
- result.puts "Feature '#{Spinach::Support.escape_single_commas name}' do"
46
+ result.puts "class #{Spinach::Support.camelize name} < Spinach::FeatureSteps"
47
+ result.puts " feature \'#{Spinach::Support.escape_single_commas name}\'\n"
47
48
  generated_steps = steps.map do |step|
48
49
  step_generator = Generators::StepGenerator.new(step)
49
50
  step_generator.generate.split("\n").map do |line|
@@ -5,7 +5,7 @@ module Spinach
5
5
  module Generators
6
6
  # Binds the feature generator to the "feature not found" hook
7
7
  def self.bind
8
- Spinach::Runner::Feature.when_not_found do |data|
8
+ Spinach.hooks.on_undefined_feature do |data|
9
9
  Spinach::Generators.generate_feature(data)
10
10
  end
11
11
  end
@@ -0,0 +1,81 @@
1
+ module Spinach
2
+ # The hookable module includes subscription capabilities to the class in which
3
+ # it is included.
4
+ #
5
+ # Take in account that while most subscription/notification mechanism work
6
+ # at the class level, Hookable defines hooks at the instance level - so they
7
+ # are not the same in all the class instances.
8
+ module Hookable
9
+
10
+ def self.included(base)
11
+ base.class_eval do
12
+ extend ClassMethods
13
+ include InstanceMethods
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ # Adds a new hook to this class. Every hook defines two methods used to
19
+ # add new callbacks and to run them passing a bunch of parameters.
20
+ #
21
+ # @example
22
+ # class
23
+ def hook(hook)
24
+ define_method hook do |&block|
25
+ add_hook(hook, &block)
26
+ end
27
+ define_method "run_#{hook}" do |*args|
28
+ run_hook(hook, *args)
29
+ end
30
+ end
31
+ end
32
+
33
+ module InstanceMethods
34
+ attr_writer :hooks
35
+
36
+ # @return [Hash]
37
+ # hash in which the key is the hook name and the value an array of any
38
+ # defined callbacks, or nil.
39
+ def hooks
40
+ @hooks ||= {}
41
+ end
42
+
43
+ # Resets all this class' hooks to a pristine state
44
+ def reset
45
+ self.hooks = {}
46
+ end
47
+
48
+ # Runs a particular hook given a set of arguments
49
+ #
50
+ # @param [String] name
51
+ # the hook's name
52
+ #
53
+ def run_hook(name, *args)
54
+ if callbacks = hooks[name.to_sym]
55
+ callbacks.each{ |c| c.call(*args) }
56
+ end
57
+ end
58
+
59
+ # @param [String] name
60
+ # the hook's identifier
61
+ #
62
+ # @return [Array]
63
+ # array of hooks for that particular identifier
64
+ def hooks_for(name)
65
+ hooks[name.to_sym] || []
66
+ end
67
+
68
+ # Adds a hook to the queue
69
+ #
70
+ # @param [String] name
71
+ # the hook's identifier
72
+ #
73
+ # @param [Proc] block
74
+ # an action to perform once that hook is executed
75
+ def add_hook(name, &block)
76
+ hooks[name.to_sym] ||= []
77
+ hooks[name.to_sym] << block
78
+ end
79
+ end
80
+ end
81
+ end