diff options
author | José Valim <jose.valim@gmail.com> | 2009-12-01 13:00:34 -0200 |
---|---|---|
committer | José Valim <jose.valim@gmail.com> | 2009-12-01 13:00:34 -0200 |
commit | c2e97cb410d759f383d29920165abdbf4b70e019 (patch) | |
tree | b375de84add24ee4c4551deec533b0667512bf34 | |
parent | fc3629f6ca2b43693f5447a1fb43881f1814e117 (diff) | |
parent | 6ac32a83283f46b55675ddf4ecab6c91f6f8abde (diff) | |
download | rails-c2e97cb410d759f383d29920165abdbf4b70e019.tar.gz rails-c2e97cb410d759f383d29920165abdbf4b70e019.tar.bz2 rails-c2e97cb410d759f383d29920165abdbf4b70e019.zip |
Merge branch 'master' of git://github.com/rails/rails
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | actionmailer/lib/action_mailer/delivery_method/smtp.rb | 2 | ||||
-rw-r--r-- | actionmailer/test/mail_service_test.rb | 34 | ||||
-rw-r--r-- | actionmailer/test/url_test.rb | 44 | ||||
-rw-r--r-- | actionpack/actionpack.gemspec | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 279 | ||||
-rw-r--r-- | actionpack/lib/action_view.rb | 2 | ||||
-rw-r--r-- | actionpack/test/controller/caching_test.rb | 4 | ||||
-rw-r--r-- | actionpack/test/dispatch/routing_test.rb | 8 | ||||
-rw-r--r-- | activesupport/lib/active_support/notifications.rb | 168 | ||||
-rw-r--r-- | activesupport/lib/active_support/notifications/fanout.rb | 101 | ||||
-rw-r--r-- | activesupport/lib/active_support/notifications/instrumenter.rb | 47 | ||||
-rw-r--r-- | activesupport/test/notifications_test.rb | 293 | ||||
m--------- | rack-mount | 0 | ||||
-rw-r--r-- | railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt | 2 | ||||
-rw-r--r-- | railties/test/application/notifications_test.rb | 30 |
17 files changed, 545 insertions, 476 deletions
diff --git a/.gitmodules b/.gitmodules index 20ed3ed9c9..fd4fd34d3e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "arel"] path = arel url = git://github.com/rails/arel.git -[submodule "rack-mount"] - path = rack-mount - url = git://github.com/rails/rack-mount.git diff --git a/actionmailer/lib/action_mailer/delivery_method/smtp.rb b/actionmailer/lib/action_mailer/delivery_method/smtp.rb index 86b0ae8329..95c117c9e0 100644 --- a/actionmailer/lib/action_mailer/delivery_method/smtp.rb +++ b/actionmailer/lib/action_mailer/delivery_method/smtp.rb @@ -16,7 +16,7 @@ module ActionMailer def perform_delivery(mail) destinations = mail.destinations mail.ready_to_send - sender = (mail['return-path'] && mail['return-path'].spec) || mail['from'] + sender = (mail['return-path'] && mail['return-path'].spec) || Array(mail.from).first smtp = Net::SMTP.new(settings[:address], settings[:port]) smtp.enable_starttls_auto if settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto) diff --git a/actionmailer/test/mail_service_test.rb b/actionmailer/test/mail_service_test.rb index c98f0a7601..697265b8ec 100644 --- a/actionmailer/test/mail_service_test.rb +++ b/actionmailer/test/mail_service_test.rb @@ -31,6 +31,18 @@ class TestMailer < ActionMailer::Base render :text => "Goodbye, Mr. #{recipient}" end + def from_with_name + from "System <system@loudthinking.com>" + recipients "root@loudthinking.com" + body "Nothing to see here." + end + + def from_without_name + from "system@loudthinking.com" + recipients "root@loudthinking.com" + body "Nothing to see here." + end + def cc_bcc(recipient) recipients recipient subject "testing bcc/cc" @@ -487,6 +499,28 @@ class ActionMailerTest < Test::Unit::TestCase assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end + def test_from_without_name_for_smtp + ActionMailer::Base.delivery_method = :smtp + TestMailer.deliver_from_without_name + + mail = MockSMTP.deliveries.first + assert_not_nil mail + mail, from, to = mail + + assert_equal 'system@loudthinking.com', from.to_s + end + + def test_from_with_name_for_smtp + ActionMailer::Base.delivery_method = :smtp + TestMailer.deliver_from_with_name + + mail = MockSMTP.deliveries.first + assert_not_nil mail + mail, from, to = mail + + assert_equal 'system@loudthinking.com', from.to_s + end + def test_reply_to expected = new_mail diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index 2224f6321c..18eb355e7d 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -1,7 +1,9 @@ require 'abstract_unit' -class TestMailer < ActionMailer::Base +class WelcomeController < ActionController::Base +end +class TestMailer < ActionMailer::Base default_url_options[:host] = 'www.basecamphq.com' def signed_up_with_url(recipient) @@ -52,27 +54,25 @@ class ActionMailerUrlTest < Test::Unit::TestCase end def test_signed_up_with_url - ActionController::Routing.use_controllers! ['welcome'] do - ActionController::Routing::Routes.draw do |map| - map.connect ':controller/:action/:id' - map.welcome 'welcome', :controller=>"foo", :action=>"bar" - end - - expected = new_mail - expected.to = @recipient - expected.subject = "[Signed up] Welcome #{@recipient}" - expected.body = "Hello there, \n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n<img alt=\"Somelogo\" src=\"/images/somelogo.png\" />" - expected.from = "system@loudthinking.com" - expected.date = Time.local(2004, 12, 12) - - created = nil - assert_nothing_raised { created = TestMailer.create_signed_up_with_url(@recipient) } - assert_not_nil created - assert_equal expected.encoded, created.encoded - - assert_nothing_raised { TestMailer.deliver_signed_up_with_url(@recipient) } - assert_not_nil ActionMailer::Base.deliveries.first - assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded + ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id' + map.welcome 'welcome', :controller=>"foo", :action=>"bar" end + + expected = new_mail + expected.to = @recipient + expected.subject = "[Signed up] Welcome #{@recipient}" + expected.body = "Hello there, \n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n<img alt=\"Somelogo\" src=\"/images/somelogo.png\" />" + expected.from = "system@loudthinking.com" + expected.date = Time.local(2004, 12, 12) + + created = nil + assert_nothing_raised { created = TestMailer.create_signed_up_with_url(@recipient) } + assert_not_nil created + assert_equal expected.encoded, created.encoded + + assert_nothing_raised { TestMailer.deliver_signed_up_with_url(@recipient) } + assert_not_nil ActionMailer::Base.deliveries.first + assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded end end diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index ccf6bf1e83..936279ad12 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| s.add_dependency('activemodel', '= 3.0.pre') s.add_dependency('rack', '~> 1.0.1') s.add_dependency('rack-test', '~> 0.5.0') - s.add_dependency('rack-mount', '~> 0.2.2') + s.add_dependency('rack-mount', '~> 0.2.3') s.add_dependency('erubis', '~> 2.6.5') s.require_path = 'lib' diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb index f8f6b424ca..07b4919934 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb @@ -7,7 +7,7 @@ names = traces.collect {|name, trace| name} %> -<p><code>RAILS_ROOT: <%= defined?(RAILS_ROOT) ? RAILS_ROOT : "unset" %></code></p> +<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p> <div id="traces"> <% names.each do |name| %> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index cfe7425a61..34d75e55b6 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2,8 +2,73 @@ module ActionDispatch module Routing class Mapper module Resources + class Resource #:nodoc: + attr_reader :plural, :singular + attr_reader :path_prefix, :name_prefix + + def initialize(entities, options = {}) + entities = entities.to_s + + @plural = entities.pluralize + @singular = entities.singularize + + @path_prefix = options[:path_prefix] + @name_prefix = options[:name_prefix] + end + + def name + plural + end + + def controller + plural + end + + def member_name + if name_prefix + "#{name_prefix}_#{singular}" + else + singular + end + end + + def collection_name + if name_prefix + "#{name_prefix}_#{plural}" + else + plural + end + end + + def new_name + if name_prefix + "new_#{name_prefix}_#{singular}" + else + "new_#{singular}" + end + end + + def edit_name + if name_prefix + "edit_#{name_prefix}_#{singular}" + else + "edit_#{singular}" + end + end + end + + class SingletonResource < Resource #:nodoc: + def initialize(entity, options = {}) + super(entity) + end + + def name + singular + end + end + def resource(*resources, &block) - options = resources.last.is_a?(Hash) ? resources.pop : {} + options = resources.extract_options! if resources.length > 1 raise ArgumentError if block_given? @@ -11,29 +76,31 @@ module ActionDispatch return self end - resource = resources.pop + name_prefix = @scope[:options][:name_prefix] if @scope[:options] + resource = SingletonResource.new(resources.pop, :name_prefix => name_prefix) if @scope[:scope_level] == :resources - member do - resource(resource, options, &block) + parent_resource = @scope[:scope_level_options][:name] + parent_named_prefix = @scope[:scope_level_options][:name_prefix] + with_scope_level(:member) do + scope(":#{parent_resource}_id", :name_prefix => parent_named_prefix) do + resource(resource.name, options, &block) + end end return self end - singular = resource.to_s - plural = singular.pluralize - - controller(plural) do - namespace(resource) do - with_scope_level(:resource) do + controller(resource.controller) do + namespace(resource.name) do + with_scope_level(:resource, :name => resource.singular, :name_prefix => resource.member_name) do yield if block_given? - get "", :to => :show, :as => "#{singular}" + get "", :to => :show, :as => resource.member_name post "", :to => :create put "", :to => :update delete "", :to => :destroy - get "new", :to => :new, :as => "new_#{singular}" - get "edit", :to => :edit, :as => "edit_#{singular}" + get "new", :to => :new, :as => resource.new_name + get "edit", :to => :edit, :as => resource.edit_name end end end @@ -42,7 +109,7 @@ module ActionDispatch end def resources(*resources, &block) - options = resources.last.is_a?(Hash) ? resources.pop : {} + options = resources.extract_options! if resources.length > 1 raise ArgumentError if block_given? @@ -50,42 +117,36 @@ module ActionDispatch return self end - resource = resources.pop - - plural = resource.to_s - singular = plural.singularize + name_prefix = @scope[:options][:name_prefix] if @scope[:options] + resource = Resource.new(resources.pop, :name_prefix => name_prefix) if @scope[:scope_level] == :resources parent_resource = @scope[:scope_level_options][:name] + parent_named_prefix = @scope[:scope_level_options][:name_prefix] with_scope_level(:member) do - scope(":#{parent_resource}_id", :name_prefix => parent_resource) do - resources(resource, options, &block) + scope(":#{parent_resource}_id", :name_prefix => parent_named_prefix) do + resources(resource.name, options, &block) end end return self end - if @scope[:options] && (prefix = @scope[:options][:name_prefix]) - plural = "#{prefix}_#{plural}" - singular = "#{prefix}_#{singular}" - end - - controller(resource) do - namespace(resource) do - with_scope_level(:resources, :name => singular) do + controller(resource.controller) do + namespace(resource.name) do + with_scope_level(:resources, :name => resource.singular, :name_prefix => resource.member_name) do yield if block_given? + collection do + get "", :to => :index, :as => resource.collection_name + post "", :to => :create + get "new", :to => :new, :as => resource.new_name + end + member do - get "", :to => :show, :as => singular + get "", :to => :show, :as => resource.member_name put "", :to => :update delete "", :to => :destroy - get "edit", :to => :edit, :as => "edit_#{singular}" - end - - collection do - get "", :to => :index, :as => plural - post "", :to => :create - get "new", :to => :new, :as => "new_#{singular}" + get "edit", :to => :edit, :as => resource.edit_name end end end @@ -117,7 +178,7 @@ module ActionDispatch end def match(*args) - options = args.last.is_a?(Hash) ? args.pop : {} + options = args.extract_options! args.push(options) case options.delete(:on) @@ -146,8 +207,14 @@ module ActionDispatch end module Scoping + def self.extended(object) + object.instance_eval do + @scope = {} + end + end + def scope(*args) - options = args.last.is_a?(Hash) ? args.pop : {} + options = args.extract_options! constraints = options.delete(:constraints) || {} unless constraints.is_a?(Hash) @@ -193,9 +260,60 @@ module ActionDispatch def constraints(constraints = {}) scope(:constraints => constraints) { yield } end + + def match(*args) + options = args.extract_options! + options = (@scope[:options] || {}).merge(options) + args.push(options) + super(*args) + end + end + + module HttpHelpers + def get(*args, &block) + map_method(:get, *args, &block) + end + + def post(*args, &block) + map_method(:post, *args, &block) + end + + def put(*args, &block) + map_method(:put, *args, &block) + end + + def delete(*args, &block) + map_method(:delete, *args, &block) + end + + def redirect(path, options = {}) + status = options[:status] || 301 + lambda { |env| + req = Rack::Request.new(env) + url = req.scheme + '://' + req.host + path + [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']] + } + end + + private + def map_method(method, *args, &block) + options = args.extract_options! + options[:via] = method + args.push(options) + match(*args, &block) + self + end end class Constraints + def new(app, constraints = []) + if constraints.any? + super(app, constraints) + else + app + end + end + def initialize(app, constraints = []) @app, @constraints = app, constraints end @@ -217,34 +335,18 @@ module ActionDispatch def initialize(set) @set = set - @scope = {} + extend HttpHelpers extend Scoping extend Resources end - def get(*args, &block) - map_method(:get, *args, &block) - end - - def post(*args, &block) - map_method(:post, *args, &block) - end - - def put(*args, &block) - map_method(:put, *args, &block) - end - - def delete(*args, &block) - map_method(:delete, *args, &block) - end - def root(options = {}) match '/', options.merge(:as => :root) end def match(*args) - options = args.last.is_a?(Hash) ? args.pop : {} + options = args.extract_options! if args.length > 1 args.each { |path| match(path, options) } @@ -257,7 +359,6 @@ module ActionDispatch path = args.first - options = (@scope[:options] || {}).merge(options) conditions, defaults = {}, {} path = nil if path == "" @@ -289,51 +390,45 @@ module ActionDispatch conditions[:request_method] = Regexp.union(*via) end - defaults[:controller] = @scope[:controller].to_s if @scope[:controller] + defaults[:controller] ||= @scope[:controller].to_s if @scope[:controller] - if options[:to].respond_to?(:call) - app = options[:to] - defaults.delete(:controller) - defaults.delete(:action) - elsif options[:to].is_a?(String) - defaults[:controller], defaults[:action] = options[:to].split('#') - elsif options[:to].is_a?(Symbol) - defaults[:action] = options[:to].to_s + app = initialize_app_endpoint(options, defaults) + validate_defaults!(app, defaults, segment_keys) + app = Constraints.new(app, blocks) + + @set.add_route(app, conditions, requirements, defaults, options[:as]) + + self + end + + private + def initialize_app_endpoint(options, defaults) + app = nil + + if options[:to].respond_to?(:call) + app = options[:to] + defaults.delete(:controller) + defaults.delete(:action) + elsif options[:to].is_a?(String) + defaults[:controller], defaults[:action] = options[:to].split('#') + elsif options[:to].is_a?(Symbol) + defaults[:action] = options[:to].to_s + end + + app || Routing::RouteSet::Dispatcher.new(:defaults => defaults) end - app ||= Routing::RouteSet::Dispatcher.new(:defaults => defaults) - if app.is_a?(Routing::RouteSet::Dispatcher) + def validate_defaults!(app, defaults, segment_keys) + return unless app.is_a?(Routing::RouteSet::Dispatcher) + unless defaults.include?(:controller) || segment_keys.include?("controller") raise ArgumentError, "missing :controller" end + unless defaults.include?(:action) || segment_keys.include?("action") raise ArgumentError, "missing :action" end end - - app = Constraints.new(app, blocks) if blocks.any? - @set.add_route(app, conditions, requirements, defaults, options[:as]) - - self - end - - def redirect(path, options = {}) - status = options[:status] || 301 - lambda { |env| - req = Rack::Request.new(env) - url = req.scheme + '://' + req.host + path - [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']] - } - end - - private - def map_method(method, *args, &block) - options = args.last.is_a?(Hash) ? args.pop : {} - options[:via] = method - args.push(options) - match(*args, &block) - self - end end end end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index e95e84aeb5..f6f7ec0c8c 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -25,7 +25,7 @@ require File.join(File.dirname(__FILE__), "action_pack") module ActionView def self.load_all! - [Context, Base, InlineTemplate, TemplateError] + [Context, Base, TemplateError] end autoload :Base, 'action_view/base' diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 3ce90b6ccf..54de920740 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -632,13 +632,15 @@ class FragmentCachingTest < ActionController::TestCase def test_fragment_for_logging fragment_computed = false - ActiveSupport::Notifications.queue.expects(:publish).times(2) + events = [] + ActiveSupport::Notifications.subscribe { |*args| events << args } buffer = 'generated till now -> ' @controller.fragment_for(buffer, 'expensive') { fragment_computed = true } assert fragment_computed assert_equal 'generated till now -> ', buffer + assert_equal [:fragment_exist?, :write_fragment], events.map(&:first) end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 496445fc34..b8bcdc2808 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -251,9 +251,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/projects/1/companies/1/people' assert_equal 'people#index', @response.body - pending do - assert_equal '/projects/1/companies/1/people', project_company_people_path(:project_id => '1', :company_id => '1') - end + assert_equal '/projects/1/companies/1/people', project_company_people_path(:project_id => '1', :company_id => '1') get '/projects/1/companies/1/avatar' assert_equal 'avatars#show', @response.body @@ -345,9 +343,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/projects/1/posts/1/comments' assert_equal 'comments#index', @response.body - pending do - assert_equal '/projects/1/posts/1/comments', project_post_comments_path(:project_id => '1', :post_id => '1') - end + assert_equal '/projects/1/posts/1/comments', project_post_comments_path(:project_id => '1', :post_id => '1') post '/projects/1/posts/1/comments/preview' assert_equal 'comments#preview', @response.body diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index e2540cd598..d9bfcbfcab 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -1,7 +1,4 @@ -require 'thread' require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/secure_random' module ActiveSupport # Notifications provides an instrumentation API for Ruby. To instrument an @@ -41,173 +38,42 @@ module ActiveSupport # to subscribers in a thread. You can use any queue implementation you want. # module Notifications - mattr_accessor :queue, :listener + autoload :Instrumenter, 'active_support/notifications/instrumenter' + autoload :Event, 'active_support/notifications/instrumenter' + autoload :Fanout, 'active_support/notifications/fanout' class << self - delegate :instrument, :transaction_id, :transaction, :to => :instrumenter + attr_writer :notifier + delegate :publish, :subscribe, :instrument, :to => :notifier - def instrumenter - Thread.current[:notifications_instrumeter] ||= Instrumenter.new(publisher) - end - - def publisher - @publisher ||= Publisher.new(queue) - end - - def subscriber - @subscriber ||= Subscriber.new(queue) - end - - def subscribe(pattern=nil, options={}, &block) - with = options[:with] || listener - subscriber.bind(with, pattern).subscribe(&block) + def notifier + @notifier ||= Notifier.new end end - class Instrumenter - def initialize(publisher) - @publisher = publisher - @id = random_id - end - - def transaction - @id, old_id = random_id, @id - yield - ensure - @id = old_id - end - - def transaction_id - @id - end - - def instrument(name, payload={}) - time = Time.now - result = yield if block_given? - ensure - @publisher.publish(name, time, Time.now, result, @id, payload) - end - - private - def random_id - SecureRandom.hex(10) - end - end - - class Publisher - def initialize(queue) + class Notifier + def initialize(queue = Fanout.new) @queue = queue end def publish(*args) @queue.publish(*args) end - end - - class Subscriber - def initialize(queue) - @queue = queue - end - - def bind(listener, pattern) - @listener = listener - @pattern = pattern - self - end - def subscribe - @queue.subscribe(@listener, @pattern) do |*args| - yield(*args) - end + def subscribe(pattern = nil, &block) + @queue.bind(pattern).subscribe(&block) end - end - - class Event - attr_reader :name, :time, :end, :transaction_id, :result, :payload - def initialize(name, start, ending, result, transaction_id, payload) - @name = name - @payload = payload.dup - @time = start - @transaction_id = transaction_id - @end = ending - @result = result + def wait + @queue.wait end - def duration - @duration ||= 1000.0 * (@end - @time) - end + delegate :instrument, :to => :current_instrumenter - def parent_of?(event) - start = (self.time - event.time) * 1000 - start <= 0 && (start + duration >= event.duration) - end - end - - class AsyncListener - def initialize(pattern, &block) - @pattern = pattern - @subscriber = block - @queue = Queue.new - Thread.new { consume } - end - - def publish(name, *args) - if !@pattern || @pattern === name.to_s - @queue << args.unshift(name) - end - end - - def consume - while args = @queue.shift - @subscriber.call(*args) - end - end - - def drained? - @queue.size.zero? - end - end - - class SyncListener - def initialize(pattern, &block) - @pattern = pattern - @subscriber = block - end - - def publish(name, *args) - if !@pattern || @pattern === name.to_s - @subscriber.call(*args.unshift(name)) + private + def current_instrumenter + Thread.current[:"instrumentation_#{object_id}"] ||= Notifications::Instrumenter.new(self) end - end - - def drained? - true - end - end - - # This is a default queue implementation that ships with Notifications. It - # consumes events in a thread and publish them to all registered subscribers. - # - class LittleFanout - def initialize - @listeners = [] - end - - def publish(*args) - @listeners.each { |l| l.publish(*args) } - end - - def subscribe(listener, pattern=nil, &block) - @listeners << listener.new(pattern, &block) - end - - def drained? - @listeners.all? &:drained? - end end end - - Notifications.queue = Notifications::LittleFanout.new - Notifications.listener = Notifications::AsyncListener end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb new file mode 100644 index 0000000000..bb07e4765c --- /dev/null +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -0,0 +1,101 @@ +require 'thread' + +module ActiveSupport + module Notifications + # This is a default queue implementation that ships with Notifications. It + # consumes events in a thread and publish them to all registered subscribers. + # + class Fanout + def initialize(sync = false) + @subscriber_klass = sync ? Subscriber : AsyncSubscriber + @subscribers = [] + end + + def bind(pattern) + Binding.new(self, pattern) + end + + def subscribe(pattern = nil, &block) + @subscribers << @subscriber_klass.new(pattern, &block) + end + + def publish(*args) + @subscribers.each { |s| s.publish(*args) } + end + + def wait + sleep(0.05) until @subscribers.all?(&:drained?) + end + + # Used for internal implementation only. + class Binding #:nodoc: + def initialize(queue, pattern) + @queue = queue + @pattern = + case pattern + when Regexp, NilClass + pattern + else + /^#{Regexp.escape(pattern.to_s)}/ + end + end + + def subscribe(&block) + @queue.subscribe(@pattern, &block) + end + end + + class Subscriber #:nodoc: + def initialize(pattern, &block) + @pattern = pattern + @block = block + end + + def publish(*args) + push(*args) if matches?(args.first) + end + + def drained? + true + end + + private + def matches?(name) + !@pattern || @pattern =~ name.to_s + end + + def push(*args) + @block.call(*args) + end + end + + # Used for internal implementation only. + class AsyncSubscriber < Subscriber #:nodoc: + def initialize(pattern, &block) + super + @events = Queue.new + start_consumer + end + + def drained? + @events.empty? + end + + private + def start_consumer + Thread.new { consume } + end + + def consume + while args = @events.shift + @block.call(*args) + end + end + + def push(*args) + @events << args + end + end + end + end +end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb new file mode 100644 index 0000000000..fb95422af2 --- /dev/null +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -0,0 +1,47 @@ +require 'active_support/secure_random' +require 'active_support/core_ext/module/delegation' + +module ActiveSupport + module Notifications + class Instrumenter + def initialize(notifier) + @id = unique_id + @notifier = notifier + end + + def instrument(name, payload={}) + time = Time.now + result = yield if block_given? + ensure + @notifier.publish(name, time, Time.now, result, @id, payload) + end + + private + def unique_id + SecureRandom.hex(10) + end + end + + class Event + attr_reader :name, :time, :end, :transaction_id, :result, :payload + + def initialize(name, start, ending, result, transaction_id, payload) + @name = name + @payload = payload.dup + @time = start + @transaction_id = transaction_id + @end = ending + @result = result + end + + def duration + @duration ||= 1000.0 * (@end - @time) + end + + def parent_of?(event) + start = (self.time - event.time) * 1000 + start <= 0 && (start + duration >= event.duration) + end + end + end +end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 35d44367cf..4f880d0db7 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -1,221 +1,170 @@ require 'abstract_unit' -# Allow LittleFanout to be cleaned. -class ActiveSupport::Notifications::LittleFanout - def clear - @listeners.clear - end -end - -class NotificationsEventTest < Test::Unit::TestCase - def test_events_are_initialized_with_details - event = event(:foo, Time.now, Time.now + 1, 1, random_id, :payload => :bar) - assert_equal :foo, event.name - assert_equal Hash[:payload => :bar], event.payload - end - - def test_events_consumes_information_given_as_payload - time = Time.now - event = event(:foo, time, time + 0.01, 1, random_id, {}) - - assert_equal Hash.new, event.payload - assert_equal time, event.time - assert_equal 1, event.result - assert_equal 10.0, event.duration - end - - def test_event_is_parent_based_on_time_frame - time = Time.utc(2009, 01, 01, 0, 0, 1) - - parent = event(:foo, Time.utc(2009), Time.utc(2009) + 100, nil, random_id, {}) - child = event(:foo, time, time + 10, nil, random_id, {}) - not_child = event(:foo, time, time + 100, nil, random_id, {}) - - assert parent.parent_of?(child) - assert !child.parent_of?(parent) - assert !parent.parent_of?(not_child) - assert !not_child.parent_of?(parent) - end +module Notifications + class TestCase < ActiveSupport::TestCase + def setup + Thread.abort_on_exception = true + + @notifier = ActiveSupport::Notifications::Notifier.new + @events = [] + @notifier.subscribe { |*args| @events << event(*args) } + end -protected + def teardown + Thread.abort_on_exception = false + end - def random_id - @random_id ||= ActiveSupport::SecureRandom.hex(10) - end + private + def event(*args) + ActiveSupport::Notifications::Event.new(*args) + end - def event(*args) - ActiveSupport::Notifications::Event.new(*args) + def drain + @notifier.wait + end end -end -class NotificationsMainTest < Test::Unit::TestCase - def setup - @events = [] - Thread.abort_on_exception = true - ActiveSupport::Notifications.subscribe do |*args| - @events << ActiveSupport::Notifications::Event.new(*args) + class PubSubTest < TestCase + def test_events_are_published_to_a_listener + @notifier.publish :foo + @notifier.wait + assert_equal [[:foo]], @events end - end - def teardown - Thread.abort_on_exception = false - ActiveSupport::Notifications.queue.clear - end - - def test_notifications_returns_action_result - result = ActiveSupport::Notifications.instrument(:awesome, :payload => "notifications") do - 1 + 1 - end + def test_subscriber_with_pattern + events = [] + @notifier.subscribe('1') { |*args| events << args } - assert_equal 2, result - end + @notifier.publish '1' + @notifier.publish '1.a' + @notifier.publish 'a.1' + @notifier.wait - def test_events_are_published_to_a_listener - ActiveSupport::Notifications.instrument(:awesome, :payload => "notifications") do - 1 + 1 + assert_equal [['1'], ['1.a']], events end - drain - - assert_equal 1, @events.size - assert_equal :awesome, @events.last.name - assert_equal Hash[:payload => "notifications"], @events.last.payload - end + def test_subscriber_with_pattern_as_regexp + events = [] + @notifier.subscribe(/\d/) { |*args| events << args } - def test_nested_events_can_be_instrumented - ActiveSupport::Notifications.instrument(:awesome, :payload => "notifications") do - ActiveSupport::Notifications.instrument(:wot, :payload => "child") do - 1 + 1 - end + @notifier.publish '1' + @notifier.publish 'a.1' + @notifier.publish '1.a' + @notifier.wait - drain - - assert_equal 1, @events.size - assert_equal :wot, @events.first.name - assert_equal Hash[:payload => "child"], @events.first.payload + assert_equal [['1'], ['a.1'], ['1.a']], events end - drain + def test_multiple_subscribers + @another = [] + @notifier.subscribe { |*args| @another << args } + @notifier.publish :foo + @notifier.wait - assert_equal 2, @events.size - assert_equal :awesome, @events.last.name - assert_equal Hash[:payload => "notifications"], @events.last.payload - end - - def test_event_is_pushed_even_if_block_fails - ActiveSupport::Notifications.instrument(:awesome, :payload => "notifications") do - raise "OMG" - end rescue RuntimeError - - drain + assert_equal [[:foo]], @events + assert_equal [[:foo]], @another + end - assert_equal 1, @events.size - assert_equal :awesome, @events.last.name - assert_equal Hash[:payload => "notifications"], @events.last.payload + private + def event(*args) + args + end end - def test_event_is_pushed_even_without_block - ActiveSupport::Notifications.instrument(:awesome, :payload => "notifications") - drain + class SyncPubSubTest < PubSubTest + def setup + Thread.abort_on_exception = true - assert_equal 1, @events.size - assert_equal :awesome, @events.last.name - assert_equal Hash[:payload => "notifications"], @events.last.payload + @notifier = ActiveSupport::Notifications::Notifier.new(ActiveSupport::Notifications::Fanout.new(true)) + @events = [] + @notifier.subscribe { |*args| @events << event(*args) } + end end - def test_subscribed_in_a_transaction - @another = [] - - ActiveSupport::Notifications.subscribe("cache") do |*args| - @another << ActiveSupport::Notifications::Event.new(*args) + class InstrumentationTest < TestCase + def test_instrument_returns_block_result + assert_equal 2, @notifier.instrument(:awesome) { 1 + 1 } end - ActiveSupport::Notifications.instrument(:cache){ 1 } - ActiveSupport::Notifications.transaction do - ActiveSupport::Notifications.instrument(:cache){ 1 } - end - ActiveSupport::Notifications.instrument(:cache){ 1 } + def test_nested_events_can_be_instrumented + @notifier.instrument(:awesome, :payload => "notifications") do + @notifier.instrument(:wot, :payload => "child") do + 1 + 1 + end - drain + drain - assert_equal 3, @another.size - before, during, after = @another.map {|e| e.transaction_id } - assert_equal before, after - assert_not_equal before, during - end + assert_equal 1, @events.size + assert_equal :wot, @events.first.name + assert_equal Hash[:payload => "child"], @events.first.payload + end - def test_subscriber_with_pattern - @another = [] + drain - ActiveSupport::Notifications.subscribe("cache") do |*args| - @another << ActiveSupport::Notifications::Event.new(*args) + assert_equal 2, @events.size + assert_equal :awesome, @events.last.name + assert_equal Hash[:payload => "notifications"], @events.last.payload end - ActiveSupport::Notifications.instrument(:cache){ 1 } - - drain + def test_instrument_publishes_when_exception_is_raised + begin + @notifier.instrument(:awesome, :payload => "notifications") do + raise "OMG" + end + flunk + rescue + end - assert_equal 1, @another.size - assert_equal :cache, @another.first.name - assert_equal 1, @another.first.result - end + drain - def test_subscriber_with_pattern_as_regexp - @another = [] - ActiveSupport::Notifications.subscribe(/cache/) do |*args| - @another << ActiveSupport::Notifications::Event.new(*args) + assert_equal 1, @events.size + assert_equal :awesome, @events.last.name + assert_equal Hash[:payload => "notifications"], @events.last.payload end - ActiveSupport::Notifications.instrument(:something){ 0 } - ActiveSupport::Notifications.instrument(:cache){ 1 } - - drain + def test_event_is_pushed_even_without_block + @notifier.instrument(:awesome, :payload => "notifications") + drain - assert_equal 1, @another.size - assert_equal :cache, @another.first.name - assert_equal 1, @another.first.result + assert_equal 1, @events.size + assert_equal :awesome, @events.last.name + assert_equal Hash[:payload => "notifications"], @events.last.payload + end end - def test_subscriber_allows_sync_listeners - @another = [] - ActiveSupport::Notifications.subscribe(/cache/, :with => ActiveSupport::Notifications::SyncListener) do |*args| - @another << ActiveSupport::Notifications::Event.new(*args) + class EventTest < TestCase + def test_events_are_initialized_with_details + event = event(:foo, Time.now, Time.now + 1, 1, random_id, :payload => :bar) + assert_equal :foo, event.name + assert_equal Hash[:payload => :bar], event.payload end - Thread.expects(:new).never - ActiveSupport::Notifications.instrument(:something){ 0 } - ActiveSupport::Notifications.instrument(:cache){ 1 } + def test_events_consumes_information_given_as_payload + time = Time.now + event = event(:foo, time, time + 0.01, 1, random_id, {}) - assert_equal 1, @another.size - assert_equal :cache, @another.first.name - assert_equal 1, @another.first.result - end - - def test_with_several_consumers_and_several_events - @another = [] - ActiveSupport::Notifications.subscribe do |*args| - @another << ActiveSupport::Notifications::Event.new(*args) + assert_equal Hash.new, event.payload + assert_equal time, event.time + assert_equal 1, event.result + assert_equal 10.0, event.duration end - 1.upto(100) do |i| - ActiveSupport::Notifications.instrument(:value){ i } - end + def test_event_is_parent_based_on_time_frame + time = Time.utc(2009, 01, 01, 0, 0, 1) - drain + parent = event(:foo, Time.utc(2009), Time.utc(2009) + 100, nil, random_id, {}) + child = event(:foo, time, time + 10, nil, random_id, {}) + not_child = event(:foo, time, time + 100, nil, random_id, {}) - assert_equal 100, @events.size - assert_equal :value, @events.first.name - assert_equal 1, @events.first.result - assert_equal 100, @events.last.result + assert parent.parent_of?(child) + assert !child.parent_of?(parent) + assert !parent.parent_of?(not_child) + assert !not_child.parent_of?(parent) + end - assert_equal 100, @another.size - assert_equal :value, @another.first.name - assert_equal 1, @another.first.result - assert_equal 100, @another.last.result + protected + def random_id + @random_id ||= ActiveSupport::SecureRandom.hex(10) + end end - - private - def drain - sleep(0.05) until ActiveSupport::Notifications.queue.drained? - end end diff --git a/rack-mount b/rack-mount deleted file mode 160000 -Subproject 3784e633b42f43a4131e02519be60080d179da2 diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt index 675f00043f..d8757460e4 100644 --- a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt +++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt @@ -1,5 +1,5 @@ class <%= class_name %>Generator < Rails::Generators::NamedBase def self.source_root - @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates')) + @source_root ||= File.expand_path('../templates', __FILE__) end end diff --git a/railties/test/application/notifications_test.rb b/railties/test/application/notifications_test.rb index 28dfdfcd83..71e406f2c1 100644 --- a/railties/test/application/notifications_test.rb +++ b/railties/test/application/notifications_test.rb @@ -5,21 +5,8 @@ module ApplicationTests include ActiveSupport::Testing::Isolation class MyQueue - attr_reader :events, :subscribers - - def initialize - @events = [] - @subscribers = [] - @listeners = [] - end - def publish(name, *args) - @events << name - end - - def subscribe(listener, pattern=nil, &block) - @listeners << listener - @subscribers << pattern + raise name end end @@ -28,21 +15,16 @@ module ApplicationTests boot_rails require "rails" require "active_support/notifications" + @events = [] Rails::Initializer.run do |c| - c.notifications.queue = MyQueue.new - c.notifications.subscribe(/listening/) do - puts "Cool" - end + c.notifications.notifier = ActiveSupport::Notifications::Notifier.new(MyQueue.new) end end test "new queue is set" do - ActiveSupport::Notifications.instrument(:foo) - assert_equal :foo, ActiveSupport::Notifications.queue.events.first - end - - test "configuration subscribers are loaded" do - assert_equal 1, ActiveSupport::Notifications.queue.subscribers.count { |s| s == /listening/ } + assert_raise RuntimeError do + ActiveSupport::Notifications.publish('foo') + end end end end |