diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..a35edc9 --- /dev/null +++ b/.rspec @@ -0,0 +1,4 @@ +--color +--format documentation +--order rand + diff --git a/Gemfile.lock b/Gemfile.lock index 1aa83b1..ec7460b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,9 +33,18 @@ GEM rack-protection (1.5.3) rack rake (10.5.0) + rspec (3.4.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-core (3.4.4) + rspec-support (~> 3.4.0) rspec-expectations (3.4.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.4.0) + rspec-mocks (3.4.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) rspec-support (3.4.1) sinatra (1.4.7) rack (~> 1.5) @@ -51,4 +60,5 @@ DEPENDENCIES bundler (~> 1.6) dredd_hooks! rake (~> 10.0) + rspec (~> 3.0) sinatra (~> 1.4.5) diff --git a/README.md b/README.md index 866b93b..4fb48f2 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,29 @@ Development rake ``` +A few [Cucumber][cucumber] features provide an end-to-end test harness, and a set of [RSpec][rspec] specs provide both a more granular documentation and a unit test harness. + +RSpec [tags][tags] are used to categorize the spec examples. + +Spec examples that are tagged as `public` describe aspects of the gem public API, and MAY be considered as its documentation. + +The `private` or `protected` specs are written for development purpose only. Because they describe internal behaviour which may change at any moment without notice, they are only executed as a secondary task by the [continuous integration service][travis] and SHOULD be ignored. + +Run `rake spec:public` to print the gem public documentation. + + [cucumber]: https://github.com/cucumber/cucumber-rails + [rspec]: https://www.relishapp.com/rspec + [tags]: https://www.relishapp.com/rspec/rspec-core/v/3-4/docs/command-line/tag-option + [travis]: https://travis-ci.org/gonzalo-bulnes/simple_token_authentication/builds + +### Maintenance + +Extending the DSL to support new hooks is meant to be easy, see the [maintenance documentation][doc-maintenance] for details. : ) + + [doc-maintenance]: ./doc/README.md + +> Refactored with [love, internet style](https://www.youtube.com/watch?v=Xe1TZaElTAs). + Contributing ------------ diff --git a/Rakefile b/Rakefile index 2b4df61..f1524c0 100644 --- a/Rakefile +++ b/Rakefile @@ -3,5 +3,31 @@ require "cucumber/rake/task" Cucumber::Rake::Task.new -task default: [:cucumber] +begin + require 'rspec/core/rake_task' + + desc 'Provide private interfaces documentation' + RSpec::Core::RakeTask.new(:spec) + + namespace :spec do + desc 'Provide public interfaces documentation' + RSpec::Core::RakeTask.new(:public) do |t| + t.rspec_opts = "--tag public" + end + end + + namespace :spec do + desc 'Provide private interfaces documentation for development purpose' + RSpec::Core::RakeTask.new(:development) do |t| + t.rspec_opts = "--tag protected --tag private" + end + end +rescue LoadError + desc 'RSpec rake task not available' + task :spec do + abort 'RSpec rake task is not available. Be sure to install rspec-core as a gem or plugin' + end +end + +task default: ['spec:public', 'spec:development', :cucumber] diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..5c0d480 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,27 @@ +How to Add New Hooks +==================== + +Dredd does support new hooks? It's time to extend the Ruby DSL! + +Most of the new hooks definition is automated, but not everything yet. +In order to enable your new hook in the DSL (`DreddHook::Methods`) and the `DreddHooks::Runner`: + +1. Determine if the hook is specific to a transaction or applies to all of them +1. Add the _registration_ and _run_ method to the [runner spec][runner-spec] +1. Add the DSL method to the [DSL spec][methods-spec] +1. Add the usage example to the [**Execution order** feature][feature] +1. Run the entire test suite and watch the tests fail (start worrying if they don't!) +1. Add the hook name to the corresponding list in the [definitions file][def] +1. Add the corresponding Dredd **event** to the [server][server] +1. Run the test suite and watch it pass : ) + +Finally, bump the [_minor_][semver] version number, update the `README`, the `CHANGELOG` and do anything you need to do in order to release! + + [def]: ../lib/dredd_hooks/definitions.rb + [server]: ../lib/dredd_hooks/server.rb + + [runner-spec]: ../spec/lib/dredd_hooks/runner_spec.rb + [methods-spec]: ../spec/lib/dredd_hooks/methods_spec.rb + [feature]: ../features/execution_order.feature + [semver]: http://semver.org + diff --git a/dredd_hooks.gemspec b/dredd_hooks.gemspec index e39a7b7..b03ffd0 100644 --- a/dredd_hooks.gemspec +++ b/dredd_hooks.gemspec @@ -14,11 +14,12 @@ Gem::Specification.new do |spec| spec.license = "MIT" spec.executables = "dredd-hooks-ruby" - spec.files = Dir["{bin,lib}/**/*", "CHANGELOG.md", "Gemfile", "LICENSE.txt", "Rakefile", "README.md" ] - spec.test_files = Dir["features/**/*"] + spec.files = Dir["{bin,doc,lib}/**/*", "CHANGELOG.md", "Gemfile", "LICENSE.txt", "Rakefile", "README.md" ] + spec.test_files = Dir["{features,spec}/**/*"] spec.add_development_dependency "aruba", "~> 0.6.2" spec.add_development_dependency "bundler", "~> 1.6" spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "sinatra", "~> 1.4.5" end diff --git a/features/support/env.rb b/features/support/env.rb index 69c5201..1673967 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -5,5 +5,5 @@ puts "Killing server..." system "for i in `ps axu | grep 'server.rb' | grep ruby | awk '{print $2}'`; do kill -9 $i; done > /dev/null 2>&1" - @aruba_timeout_seconds = 10 + @aruba_timeout_seconds = 20 end diff --git a/lib/dredd_hooks/definitions.rb b/lib/dredd_hooks/definitions.rb new file mode 100644 index 0000000..e085f83 --- /dev/null +++ b/lib/dredd_hooks/definitions.rb @@ -0,0 +1,8 @@ +module DreddHooks + + HOOKS_ON_SINGLE_TRANSACTIONS = [:before, :before_validation, :after] + + HOOKS_ON_MULTIPLE_TRANSACTIONS = [:before_each, :before_each_validation, + :after_each, :before_all, :after_all] +end + diff --git a/lib/dredd_hooks/methods.rb b/lib/dredd_hooks/methods.rb index 8f25542..f5a9678 100644 --- a/lib/dredd_hooks/methods.rb +++ b/lib/dredd_hooks/methods.rb @@ -1,53 +1,59 @@ -module DreddHooks - module Methods - @@before_hooks = {} - @@before_validation_hooks = {} - @@after_hooks = {} +require 'dredd_hooks/definitions' +require 'dredd_hooks/runner' - @@before_each_hooks = [] - @@before_each_validation_hooks = [] - @@after_each_hooks = [] +module DreddHooks - @@before_all_hooks = [] - @@after_all_hooks = [] + # The Ruby hooks API + module Methods + # Define hook methods in the form of: # - # Ruby hooks API + # def before(transaction_name, &block) + # runner.register_before_hook(transaction_name, &block) + # end # + # Hooks names are defined by HOOKS_ON_SINGLE_TRANSACTIONS. + # + # Returns nothing. + def self.define_hooks_on_single_transactions + HOOKS_ON_SINGLE_TRANSACTIONS.each do |hook_name| - def before transaction_name, &block - @@before_hooks[transaction_name] = [] if @@before_hooks[transaction_name].nil? - @@before_hooks[transaction_name].push block - end + define_method hook_name do |transaction_name, &block| + runner.send("register_#{hook_name}_hook", transaction_name, &block) + end - def before_validation transaction_name, &block - @@before_validation_hooks[transaction_name] = [] if @@before_validation_hooks[transaction_name].nil? - @@before_validation_hooks[transaction_name].push block + end end + private_class_method :define_hooks_on_single_transactions - def after transaction_name, &block - @@after_hooks[transaction_name] = [] if @@after_hooks[transaction_name].nil? - @@after_hooks[transaction_name].push block - end + # Define hook methods in the form of: + # + # def before_all(&block) + # runner.register_before_all_hook(&block) + # end + # + # Hooks names are defined by HOOKS_ON_MULTIPLE_TRANSACTIONS. + # + # Returns nothing. + def self.define_hooks_on_multiple_transactions + HOOKS_ON_MULTIPLE_TRANSACTIONS.each do |hook_name| - def before_each &block - @@before_each_hooks.push block - end + define_method hook_name do |&block| + runner.send("register_#{hook_name}_hook", &block) + end - def before_each_validation &block - @@before_each_validation_hooks.push block + end end + private_class_method :define_hooks_on_multiple_transactions - def after_each &block - @@after_each_hooks.push block - end + define_hooks_on_single_transactions + define_hooks_on_multiple_transactions - def before_all &block - @@before_all_hooks.push block - end + private - def after_all &block - @@after_all_hooks.push block - end + def runner + Runner.instance + end end -end \ No newline at end of file +end + diff --git a/lib/dredd_hooks/runner.rb b/lib/dredd_hooks/runner.rb index 1f77f17..c805ed5 100644 --- a/lib/dredd_hooks/runner.rb +++ b/lib/dredd_hooks/runner.rb @@ -1,80 +1,141 @@ -require 'dredd_hooks/methods' +require 'singleton' + +require 'dredd_hooks/definitions' module DreddHooks - module Runner + # The Ruby hooks runner stores and runs the Ruby hooks + # + # It provides registration methods to be used from the DSL, + # as well as methods to run hooks by name from the server. + # + # All these methods are generated automatically according + # to the hooks definitions. + # + # Note that this class is a Singleton. Use Runner.instance to + # get references to its unique instance. + class Runner + + include Singleton + + # Define registration methods in the form of: # - # Runers for Transaction specific hooks + # def register_before_hook(transction_name, &block) + # hooks = @before_hooks || {} + # transaction_hooks = hooks.fetch(transaction_name, []) + # transaction_hooks.push(block) + # hooks[transaction_name] = transaction_hooks + # @before_hooks = hooks + # end # + # Hooks names are defined by HOOKS_ON_SINGLE_TRANSACTIONS. + # + # Returns nothing. + def self.define_registration_methods_for_hooks_on_single_transactions + HOOKS_ON_SINGLE_TRANSACTIONS.each do |hook_name| - def self.run_before_hooks_for_transaction(transaction) - transaction_name = transaction["name"] - hooks = Methods.class_variable_get("@@before_hooks")[transaction_name] || [] - hooks.each do |hook_proc| - hook_proc.call(transaction) - end - return transaction - end - - def self.run_before_validation_hooks_for_transaction(transaction) - transaction_name = transaction["name"] - hooks = Methods.class_variable_get("@@before_validation_hooks")[transaction_name] || [] - hooks.each do |hook_proc| - hook_proc.call(transaction) - end - return transaction - end + define_method "register_#{hook_name}_hook" do |transaction_name, &block| + hooks = instance_variable_get("@#{hook_name}_hooks") || {} + transaction_hooks = hooks.fetch(transaction_name, []) + transaction_hooks.push(block) + hooks[transaction_name] = transaction_hooks + instance_variable_set("@#{hook_name}_hooks", hooks) + end - def self.run_after_hooks_for_transaction(transaction) - transaction_name = transaction["name"] - hooks = Methods.class_variable_get("@@after_hooks")[transaction_name] || [] - hooks.each do |hook_proc| - hook_proc.call(transaction) end - return transaction end + private_class_method :define_registration_methods_for_hooks_on_single_transactions + # Define registration methods in the form of: # - # Runners for *_each hooks API + # def register_before_all_hook(&block) + # hooks = @before_hooks || [] + # hooks.push(block) + # @before_all_hooks = hooks + # end # + # Hooks names are defined by HOOKS_ON_MULTIPLE_TRANSACTIONS. + # + # Returns nothing. + def self.define_registration_methods_for_hooks_on_multiple_transactions + HOOKS_ON_MULTIPLE_TRANSACTIONS.each do |hook_name| - def self.run_before_each_hooks_for_transaction(transaction) - Methods.class_variable_get("@@before_each_hooks").each do |hook_proc| - hook_proc.call(transaction) - end - return transaction - end + define_method "register_#{hook_name}_hook" do |&block| + hooks = instance_variable_get("@#{hook_name}_hooks") || [] + hooks.push(block) + instance_variable_set("@#{hook_name}_hooks", hooks) + end - def self.run_before_each_validation_hooks_for_transaction(transaction) - Methods.class_variable_get("@@before_each_validation_hooks").each do |hook_proc| - hook_proc.call(transaction) end - return transaction end + private_class_method :define_registration_methods_for_hooks_on_multiple_transactions + + # Define runner methods in the form of: + # + # def run_before_hooks_for_transaction(transaction) + # hooks = @before_hooks || {} + # transaction_name = transaction['name'] + # transaction_hooks = hooks.fetch(transaction_name, []) + # transaction_hooks.each do |hook| + # hook.call(transaction) + # end + # return transaction + # end + # + # Hooks names are defined by HOOKS_ON_SINGLE_TRANSACTIONS. + # + # Returns nothing. + def self.define_runners_for_hooks_on_single_transactions + HOOKS_ON_SINGLE_TRANSACTIONS.each do |hook_name| + + define_method "run_#{hook_name}_hooks_for_transaction" do |transaction| + hooks = instance_variable_get("@#{hook_name}_hooks") || {} + transaction_name = transaction['name'] + transaction_hooks = hooks.fetch(transaction_name, []) + transaction_hooks.each do |hook| + hook.call(transaction) + end + return transaction + end - def self.run_after_each_hooks_for_transaction(transaction) - Methods.class_variable_get("@@after_each_hooks").each do |hook_proc| - hook_proc.call(transaction) end - return transaction end + private_class_method :define_runners_for_hooks_on_single_transactions + # Define runner methods in the form of: + # + # def run_before_all_hooks_for_transaction(transaction) + # hooks = @before_all_hooks || [] + # hooks.each do |hook| + # hook.call(transaction) + # end + # return transaction + # end # - # Runners for *_all hooks API + # Hooks names are defined by HOOKS_ON_MULTIPLE_TRANSACTIONS. # + # Returns nothing. + def self.define_runners_for_hooks_on_multiple_transactions + HOOKS_ON_MULTIPLE_TRANSACTIONS.each do |hook_name| - def self.run_before_all_hooks_for_transaction(transaction) - Methods.class_variable_get("@@before_all_hooks").each do |hook_proc| - hook_proc.call(transaction) - end - return transaction - end + define_method "run_#{hook_name}_hooks_for_transaction" do |transaction| + hooks = instance_variable_get("@#{hook_name}_hooks") || [] + hooks.each do |hook| + hook.call(transaction) + end + return transaction + end - def self.run_after_all_hooks_for_transaction(transaction) - Methods.class_variable_get("@@after_all_hooks").each do |hook_proc| - hook_proc.call(transaction) end - return transaction end + private_class_method :define_runners_for_hooks_on_multiple_transactions + + define_registration_methods_for_hooks_on_single_transactions + define_registration_methods_for_hooks_on_multiple_transactions + + define_runners_for_hooks_on_single_transactions + define_runners_for_hooks_on_multiple_transactions + end end + diff --git a/lib/dredd_hooks/server.rb b/lib/dredd_hooks/server.rb index 9b33445..54b4a2f 100644 --- a/lib/dredd_hooks/server.rb +++ b/lib/dredd_hooks/server.rb @@ -8,12 +8,16 @@ module DreddHooks # The hooks worker server class Server + attr_reader :runner + private :runner + HOST = '127.0.0.1' PORT = 61321 MESSAGE_DELIMITER = "\n" def initialize @server = TCPServer.new HOST, PORT + @runner = Runner.instance end def process_message message, client @@ -21,26 +25,26 @@ def process_message message, client transaction = message['data'] if event == "beforeEach" - transaction = DreddHooks::Runner.run_before_each_hooks_for_transaction(transaction) - transaction = DreddHooks::Runner.run_before_hooks_for_transaction(transaction) + transaction = runner.run_before_each_hooks_for_transaction(transaction) + transaction = runner.run_before_hooks_for_transaction(transaction) end if event == "beforeEachValidation" - transaction = DreddHooks::Runner.run_before_each_validation_hooks_for_transaction(transaction) - transaction = DreddHooks::Runner.run_before_validation_hooks_for_transaction(transaction) + transaction = runner.run_before_each_validation_hooks_for_transaction(transaction) + transaction = runner.run_before_validation_hooks_for_transaction(transaction) end if event == "afterEach" - transaction = DreddHooks::Runner.run_after_hooks_for_transaction(transaction) - transaction = DreddHooks::Runner.run_after_each_hooks_for_transaction(transaction) + transaction = runner.run_after_hooks_for_transaction(transaction) + transaction = runner.run_after_each_hooks_for_transaction(transaction) end if event == "beforeAll" - transaction = DreddHooks::Runner.run_before_all_hooks_for_transaction(transaction) + transaction = runner.run_before_all_hooks_for_transaction(transaction) end if event == "afterAll" - transaction = DreddHooks::Runner.run_after_all_hooks_for_transaction(transaction) + transaction = runner.run_after_all_hooks_for_transaction(transaction) end to_send = { diff --git a/spec/lib/dredd_hooks/methods_spec.rb b/spec/lib/dredd_hooks/methods_spec.rb new file mode 100644 index 0000000..038971b --- /dev/null +++ b/spec/lib/dredd_hooks/methods_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +require 'dredd_hooks/methods' + +module DreddHooks + + class DummyHooksFile + include DreddHooks::Methods + end + + describe Methods do + + let(:hooks_file) { DummyHooksFile.new } + + it 'defines #before', public: true do + expect(hooks_file).to respond_to :before + end + + it 'defines #before_validation', public: true do + expect(hooks_file).to respond_to :before_validation + end + + it 'defines #after', public: true do + expect(hooks_file).to respond_to :after + end + + it 'defines #before_each', public: true do + expect(hooks_file).to respond_to :before_each + end + + it 'defines #before_each_validation', public: true do + expect(hooks_file).to respond_to :before_each_validation + end + + it 'defines #after_each', public: true do + expect(hooks_file).to respond_to :after_each + end + + it 'defines #before_all', public: true do + expect(hooks_file).to respond_to :before_all + end + + it 'defines #after_all', public: true do + expect(hooks_file).to respond_to :after_all + end + + it 'does not expose its #runner', private: true do + expect(hooks_file).not_to respond_to :runner + end + end +end + diff --git a/spec/lib/dredd_hooks/runner_spec.rb b/spec/lib/dredd_hooks/runner_spec.rb new file mode 100644 index 0000000..241b48d --- /dev/null +++ b/spec/lib/dredd_hooks/runner_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +module DreddHooks + describe Runner do + + let(:runner) { Runner.instance } + + describe 'exposes an interface to run hooks', protected: true do + + it { expect(runner).to respond_to :run_before_hooks_for_transaction } + it { expect(runner).to respond_to :run_before_validation_hooks_for_transaction } + it { expect(runner).to respond_to :run_after_hooks_for_transaction } + it { expect(runner).to respond_to :run_before_each_hooks_for_transaction } + it { expect(runner).to respond_to :run_before_each_validation_hooks_for_transaction } + it { expect(runner).to respond_to :run_after_each_hooks_for_transaction } + it { expect(runner).to respond_to :run_before_all_hooks_for_transaction } + it { expect(runner).to respond_to :run_after_all_hooks_for_transaction } + end + + describe 'exposes an interface to register hooks', private: true do + + it { expect(runner).to respond_to :register_before_hook } + it { expect(runner).to respond_to :register_before_validation_hook } + it { expect(runner).to respond_to :register_after_hook } + it { expect(runner).to respond_to :register_before_each_hook } + it { expect(runner).to respond_to :register_before_each_validation_hook } + it { expect(runner).to respond_to :register_after_each_hook } + it { expect(runner).to respond_to :register_before_all_hook } + it { expect(runner).to respond_to :register_after_all_hook } + end + + describe 'does not expose its generator methods', private: true do + + it { expect(runner.class).not_to respond_to :define_registration_methods_for_hooks_on_single_transactions } + it { expect(runner.class).not_to respond_to :define_registration_methods_for_hooks_on_multiple_transactions } + it { expect(runner.class).not_to respond_to :define_runners_for_hooks_on_single_transactions } + it { expect(runner.class).not_to respond_to :define_runners_for_hooks_on_multiple_transactions } + end + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..c0dcb59 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,35 @@ +require 'bundler/setup' +Bundler.setup + +require 'dredd_hooks' + +Dir["./spec/support/**/*.rb"].sort.each { |f| require f; puts f } + +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # make sure the deprecated RSpec 2 syntax is not used + config.raise_errors_for_deprecations! + + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end +end diff --git a/spec/support/.keep b/spec/support/.keep new file mode 100644 index 0000000..e69de29