diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 960ad59ef3cb4..2ee6fac28ec8c 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Refactor Plugin Loader. Add plugin lib paths early, and add lots of tests. Closes #9795 [lazyatom] + * Added --skip-timestamps to generators that produce models #10036 [tpope] * Update Prototype to 1.6.0 and script.aculo.us to 1.8.0. [sam, madrobby] diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 0256ba96b26fc..ab6549164cc10 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -34,7 +34,7 @@ class Initializer # The set of loaded plugins. attr_reader :loaded_plugins - + # Runs the initializer. By default, this will invoke the #process method, # which simply executes all of the initialization routines. Alternately, # you can specify explicitly which initialization routine you want: @@ -64,6 +64,7 @@ def initialize(configuration) # * #set_load_path # * #require_frameworks # * #set_autoload_paths + # * add_plugin_load_paths # * #load_environment # * #initialize_encoding # * #initialize_database @@ -83,9 +84,10 @@ def initialize(configuration) def process check_ruby_version set_load_path - + require_frameworks set_autoload_paths + add_plugin_load_paths load_environment initialize_encoding @@ -161,14 +163,20 @@ def require_frameworks def add_support_load_paths end + # Adds all load paths from plugins to the global set of load paths, so that + # code from plugins can be required (explicitly or automatically via Dependencies). + def add_plugin_load_paths + plugin_loader.add_plugin_load_paths + end + # Loads all plugins in config.plugin_paths. plugin_paths # defaults to vendor/plugins but may also be set to a list of # paths, such as # config.plugin_paths = ["#{RAILS_ROOT}/lib/plugins", "#{RAILS_ROOT}/vendor/plugins"] # - # Each plugin discovered in plugin_paths is initialized: - # * add its +lib+ directory, if present, to the beginning of the load path - # * evaluate init.rb if present + # In the default implementation, as each plugin discovered in plugin_paths is initialized: + # * its +lib+ directory, if present, is added to the load path (immediately after the applications lib directory) + # * init.rb is evalutated, if present # # After all plugins are loaded, duplicates are removed from the load path. # If an array of plugin names is specified in config.plugins, only those plugins will be loaded @@ -178,13 +186,11 @@ def add_support_load_paths # if config.plugins ends contains :all then the named plugins will be loaded in the given order and all other # plugins will be loaded in alphabetical order def load_plugins - configuration.plugin_locators.each do |locator| - locator.new(self).each do |plugin| - plugin.load - end - end - ensure_all_registered_plugins_are_loaded! - $LOAD_PATH.uniq! + plugin_loader.load_plugins + end + + def plugin_loader + @plugin_loader ||= configuration.plugin_loader.new(self) end # Loads the environment specified by Configuration#environment_path, which @@ -337,15 +343,6 @@ def load_application_initializers end end - private - def ensure_all_registered_plugins_are_loaded! - unless configuration.plugins.nil? - if configuration.plugins.detect {|plugin| plugin != :all && !loaded_plugins.include?( plugin)} - missing_plugins = configuration.plugins - (loaded_plugins + [:all]) - raise LoadError, "Could not locate the following plugins: #{missing_plugins.to_sentence}" - end - end - end end # The Configuration class holds all the parameters for the Initializer and diff --git a/railties/lib/rails/plugin.rb b/railties/lib/rails/plugin.rb new file mode 100644 index 0000000000000..2feb90bf14712 --- /dev/null +++ b/railties/lib/rails/plugin.rb @@ -0,0 +1,83 @@ +module Rails + + # The Plugin class should be an object which provides the following methods: + # + # * +name+ - used during initialisation to order the plugin (based on name and + # the contents of config.plugins) + # * +valid?+ - returns true if this plugin can be loaded + # * +load_paths+ - each path within the returned array will be added to the $LOAD_PATH + # * +load+ - finally 'load' the plugin. + # + # These methods are expected by the Rails::Plugin::Locator and Rails::Plugin::Loader classes. + # The default implementation returns the lib directory as its load_paths, + # and evaluates init.rb when load is called. + class Plugin + include Comparable + + attr_reader :directory, :name + + def initialize(directory) + @directory = directory + @name = File.basename(@directory) rescue nil + @loaded = false + end + + def valid? + File.directory?(directory) && (has_lib_directory? || has_init_file?) + end + + # Returns a list of paths this plugin wishes to make available in $LOAD_PATH + def load_paths + report_nonexistant_or_empty_plugin! unless valid? + has_lib_directory? ? [lib_path] : [] + end + + # Evaluates a plugin's init.rb file + def load(initializer) + report_nonexistant_or_empty_plugin! unless valid? + evaluate_init_rb(initializer) + @loaded = true + end + + def loaded? + @loaded + end + + def <=>(other_plugin) + name <=> other_plugin.name + end + + private + + def report_nonexistant_or_empty_plugin! + raise LoadError, "Can not find the plugin named: #{name}" + end + + def lib_path + File.join(directory, 'lib') + end + + def init_path + File.join(directory, 'init.rb') + end + + def has_lib_directory? + File.directory?(lib_path) + end + + def has_init_file? + File.file?(init_path) + end + + def evaluate_init_rb(initializer) + if has_init_file? + silence_warnings do + # Allow plugins to reference the current configuration object + config = initializer.configuration + + eval(IO.read(init_path), binding, init_path) + end + end + end + end +end \ No newline at end of file diff --git a/railties/lib/rails/plugin/loader.rb b/railties/lib/rails/plugin/loader.rb index 016bcc6c50622..dcc62d50b6020 100644 --- a/railties/lib/rails/plugin/loader.rb +++ b/railties/lib/rails/plugin/loader.rb @@ -1,146 +1,150 @@ +require "rails/plugin" + module Rails - module Plugin + class Plugin class Loader - include Comparable - attr_reader :initializer, :directory, :name - - class << self - def load(*args) - new(*args).load - end - end - - def initialize(initializer, directory) + attr_reader :initializer + + # Creates a new Plugin::Loader instance, associated with the given + # Rails::Initializer. This default implementation automatically locates + # all plugins, and adds all plugin load paths, when it is created. The plugins + # are then fully loaded (init.rb is evaluated) when load_plugins is called. + # + # It is the loader's responsibilty to ensure that only the plugins specified + # in the configuration are actually loaded, and that the order defined + # is respected. + def initialize(initializer) @initializer = initializer - @directory = directory - @name = File.basename(directory).to_sym end - - def load - return false if loaded? - report_nonexistant_or_empty_plugin! - add_to_load_path! - register_plugin_as_loaded - evaluate - true + + # Returns the plugins to be loaded, in the order they should be loaded. + def plugins + @plugins ||= all_plugins.select { |plugin| should_load?(plugin) }.sort { |p1, p2| order_plugins(p1, p2) } end - - def loaded? - initializer.loaded_plugins.include?(name) + + # Returns all the plugins that could be found by the current locators. + def all_plugins + @all_plugins ||= locate_plugins + @all_plugins end - - def plugin_path? - File.directory?(directory) && (has_lib_directory? || has_init_file?) + + def load_plugins + plugins.each do |plugin| + plugin.load(initializer) + register_plugin_as_loaded(plugin) + end + ensure_all_registered_plugins_are_loaded! end - def enabled? - !explicit_plugin_loading_order? || registered? - end + # Adds the load paths for every plugin into the $LOAD_PATH. Plugin load paths are + # added *after* the application's lib directory, to ensure that an application + # can always override code within a plugin. + # + # Plugin load paths are also added to Dependencies.load_paths, and Dependencies.load_once_paths. + def add_plugin_load_paths + plugins.each do |plugin| + plugin.load_paths.each do |path| + $LOAD_PATH.insert(application_lib_index + 1, path) + Dependencies.load_paths << path + Dependencies.load_once_paths << path + end + end + $LOAD_PATH.uniq! + end - def explicitly_enabled? - !explicit_plugin_loading_order? || explicitly_registered? - end + protected - def registered? - explicit_plugin_loading_order? && registered_plugins_names_plugin?(name) - end + # The locate_plugins method uses each class in config.plugin_locators to + # find the set of all plugins available to this Rails application. + def locate_plugins + configuration.plugin_locators.map { |locator| + locator.new(initializer).plugins + }.flatten + # TODO: sorting based on config.plugins + end - def explicitly_registered? - explicit_plugin_loading_order? && registered_plugins.include?(name) - end - - def plugin_does_not_exist!(plugin_name = name) - raise LoadError, "Can not find the plugin named: #{plugin_name}" - end - - private - # The plugins that have been explicitly listed with config.plugins. If this list is nil - # then it means the client does not care which plugins or in what order they are loaded, - # so we load all in alphabetical order. If it is an empty array, we load no plugins, if it is - # non empty, we load the named plugins in the order specified. - def registered_plugins - config.plugins + def register_plugin_as_loaded(plugin) + initializer.loaded_plugins << plugin end - - def registered_plugins_names_plugin?(plugin_name) - registered_plugins.include?(plugin_name) || registered_plugins.include?(:all) + + def configuration + initializer.configuration end - def explicit_plugin_loading_order? - !registered_plugins.nil? + def should_load?(plugin) + # uses Plugin#name and Plugin#valid? + enabled?(plugin) && plugin.valid? end - - def report_nonexistant_or_empty_plugin! - plugin_does_not_exist! unless plugin_path? + + def order_plugins(plugin_a, plugin_b) + if !explicit_plugin_loading_order? + plugin_a <=> plugin_b + else + if !explicitly_enabled?(plugin_a) && !explicitly_enabled?(plugin_b) + plugin_a <=> plugin_b + else + effective_order_of(plugin_a) <=> effective_order_of(plugin_b) + end + end end - def lib_path - File.join(directory, 'lib') + def effective_order_of(plugin) + if explicitly_enabled?(plugin) + registered_plugin_names.index(plugin.name) + else + registered_plugin_names.index('all') + end end - - def init_path - File.join(directory, 'init.rb') + + def application_lib_index + $LOAD_PATH.index(File.join(RAILS_ROOT, 'lib')) || 0 + end + + def enabled?(plugin) + !explicit_plugin_loading_order? || registered?(plugin) end - - def has_lib_directory? - File.directory?(lib_path) + + def explicit_plugin_loading_order? + !registered_plugin_names.nil? end - - def has_init_file? - File.file?(init_path) + + def registered?(plugin) + explicit_plugin_loading_order? && registered_plugins_names_plugin?(plugin) end - - def add_to_load_path! - # Add lib to load path *after* the application lib, to allow - # application libraries to override plugin libraries. - if has_lib_directory? - application_lib_index = $LOAD_PATH.index(application_library_path) || 0 - $LOAD_PATH.insert(application_lib_index + 1, lib_path) - Dependencies.load_paths << lib_path - Dependencies.load_once_paths << lib_path - end + + def explicitly_enabled?(plugin) + !explicit_plugin_loading_order? || explicitly_registered?(plugin) end - - def application_library_path - File.join(RAILS_ROOT, 'lib') + + def explicitly_registered?(plugin) + explicit_plugin_loading_order? && registered_plugin_names.include?(plugin.name) end - - # Allow plugins to reference the current configuration object - def config - initializer.configuration + + def registered_plugins_names_plugin?(plugin) + registered_plugin_names.include?(plugin.name) || registered_plugin_names.include?('all') end - - def register_plugin_as_loaded - initializer.loaded_plugins << name + + # The plugins that have been explicitly listed with config.plugins. If this list is nil + # then it means the client does not care which plugins or in what order they are loaded, + # so we load all in alphabetical order. If it is an empty array, we load no plugins, if it is + # non empty, we load the named plugins in the order specified. + def registered_plugin_names + configuration.plugins ? configuration.plugins.map(&:to_s) : nil end - - # Evaluate in init.rb - def evaluate - silence_warnings { eval(IO.read(init_path), binding, init_path) } if has_init_file? + + def loaded?(plugin_name) + initializer.loaded_plugins.detect { |plugin| plugin.name == plugin_name.to_s } end - - def <=>(other_plugin_loader) + + def ensure_all_registered_plugins_are_loaded! if explicit_plugin_loading_order? - if non_existent_plugin = [self, other_plugin_loader].detect { |plugin| !registered_plugins_names_plugin?(plugin.name) } - plugin_does_not_exist!(non_existent_plugin.name) - end - - if !explicitly_enabled? && !other_plugin_loader.explicitly_enabled? - name.to_s <=> other_plugin_loader.name.to_s - elsif registered_plugins.include?(:all) && (!explicitly_enabled? || !other_plugin_loader.explicitly_enabled?) - effective_index = explicitly_enabled? ? registered_plugins.index(name) : registered_plugins.index(:all) - other_effective_index = other_plugin_loader.explicitly_enabled? ? - registered_plugins.index(other_plugin_loader.name) : registered_plugins.index(:all) - - effective_index <=> other_effective_index - else - registered_plugins.index(name) <=> registered_plugins.index(other_plugin_loader.name) + if configuration.plugins.detect {|plugin| plugin != :all && !loaded?(plugin) } + missing_plugins = configuration.plugins - (plugins + [:all]) + raise LoadError, "Could not locate the following plugins: #{missing_plugins.to_sentence}" end - - else - name.to_s <=> other_plugin_loader.name.to_s end end + end end end \ No newline at end of file diff --git a/railties/lib/rails/plugin/locator.rb b/railties/lib/rails/plugin/locator.rb index 6c4f2605bb1e5..b27e904b12e02 100644 --- a/railties/lib/rails/plugin/locator.rb +++ b/railties/lib/rails/plugin/locator.rb @@ -1,15 +1,23 @@ module Rails - module Plugin + class Plugin + + # The Plugin::Locator class should be subclasses to provide custom plugin-finding + # abilities to Rails (i.e. loading plugins from Gems, etc). Each subclass should implement + # the located_plugins method, which return an array of Plugin objects that have been found. class Locator include Enumerable + attr_reader :initializer def initialize(initializer) @initializer = initializer end + # This method should return all the plugins which this Plugin::Locator can find + # These will then be used by the current Plugin::Loader, which is responsible for actually + # loading the plugins themselves def plugins - located_plugins.select(&:enabled?).sort + raise "The `plugins' method must be defined by concrete subclasses of #{self.class}" end def each(&block) @@ -19,41 +27,52 @@ def each(&block) def plugin_names plugins.map(&:name) end - - private - def located_plugins - raise "The `located_plugins' method must be defined by concrete subclasses of #{self.class}" - end end + # The Rails::Plugin::FileSystemLocator will try to locate plugins by examining the directories + # the the paths given in configuration.plugin_paths. Any plugins that can be found are returned + # in a list. + # + # The criteria for a valid plugin in this case is found in Rails::Plugin#valid?, although + # other subclasses of Rails::Plugin::Locator can of course use different conditions. class FileSystemLocator < Locator - private - def located_plugins - initializer.configuration.plugin_paths.flatten.inject([]) do |plugins, path| - plugins.concat locate_plugins_under(path) - plugins - end.flatten - end + + # Returns all the plugins which can be loaded in the filesystem, under the paths given + # by configuration.plugin_paths. + def plugins + initializer.configuration.plugin_paths.flatten.inject([]) do |plugins, path| + plugins.concat locate_plugins_under(path) + plugins + end.flatten + end + + private + + # Attempts to create a plugin from the given path. If the created plugin is valid? + # (see Rails::Plugin#valid?) then the plugin instance is returned; otherwise nil. + def create_plugin(path) + plugin = Rails::Plugin.new(path) + plugin.valid? ? plugin : nil + end - # This starts at the base path looking for directories that pass the plugin_path? test of the Plugin::Loader. - # Since plugins can be nested arbitrarily deep within an unspecified number of intermediary directories, - # this method runs recursively until it finds a plugin directory. - # - # e.g. - # - # locate_plugins_under('vendor/plugins/acts/acts_as_chunky_bacon') - # => 'acts_as_chunky_bacon' - def locate_plugins_under(base_path) - Dir.glob(File.join(base_path, '*')).inject([]) do |plugins, path| - plugin_loader = initializer.configuration.plugin_loader.new(initializer, path) - if plugin_loader.plugin_path? && plugin_loader.enabled? - plugins << plugin_loader - elsif File.directory?(path) - plugins.concat locate_plugins_under(path) - end - plugins + # This starts at the base path looking for valid plugins (see Rails::Plugin#valid?). + # Since plugins can be nested arbitrarily deep within an unspecified number of intermediary + # directories, this method runs recursively until it finds a plugin directory, e.g. + # + # locate_plugins_under('vendor/plugins/acts/acts_as_chunky_bacon') + # => + # + def locate_plugins_under(base_path) + Dir.glob(File.join(base_path, '*')).inject([]) do |plugins, path| + if plugin = create_plugin(path) + plugins << plugin + elsif File.directory?(path) + plugins.concat locate_plugins_under(path) end + plugins end + end + end end end \ No newline at end of file diff --git a/railties/test/initializer_test.rb b/railties/test/initializer_test.rb index 5707a63258309..156f670e87c41 100644 --- a/railties/test/initializer_test.rb +++ b/railties/test/initializer_test.rb @@ -135,3 +135,84 @@ def assert_framework_path(path) end end end + +uses_mocha "Initializer plugin loading tests" do + require File.dirname(__FILE__) + '/plugin_test_helper' + + class InitializerPluginLoadingTests < Test::Unit::TestCase + def setup + @configuration = Rails::Configuration.new + @configuration.plugin_paths << plugin_fixture_root_path + @initializer = Rails::Initializer.new(@configuration) + @valid_plugin_path = plugin_fixture_path('default/stubby') + @empty_plugin_path = plugin_fixture_path('default/empty') + end + + def test_no_plugins_are_loaded_if_the_configuration_has_an_empty_plugin_list + only_load_the_following_plugins! [] + @initializer.load_plugins + assert_equal [], @initializer.loaded_plugins + end + + def test_only_the_specified_plugins_are_located_in_the_order_listed + plugin_names = [:plugin_with_no_lib_dir, :acts_as_chunky_bacon] + only_load_the_following_plugins! plugin_names + load_plugins! + assert_plugins plugin_names, @initializer.loaded_plugins + end + + def test_all_plugins_are_loaded_when_registered_plugin_list_is_untouched + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + load_plugins! + assert_plugins [:a, :acts_as_chunky_bacon, :plugin_with_no_lib_dir, :stubby], @initializer.loaded_plugins, failure_tip + end + + def test_all_plugins_loaded_when_all_is_used + plugin_names = [:stubby, :acts_as_chunky_bacon, :all] + only_load_the_following_plugins! plugin_names + load_plugins! + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:stubby, :acts_as_chunky_bacon, :a, :plugin_with_no_lib_dir], @initializer.loaded_plugins, failure_tip + end + + def test_all_plugins_loaded_after_all + plugin_names = [:stubby, :all, :acts_as_chunky_bacon] + only_load_the_following_plugins! plugin_names + load_plugins! + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:stubby, :a, :plugin_with_no_lib_dir, :acts_as_chunky_bacon], @initializer.loaded_plugins, failure_tip + end + + def test_plugin_names_may_be_strings + plugin_names = ['stubby', 'acts_as_chunky_bacon', :a, :plugin_with_no_lib_dir] + only_load_the_following_plugins! plugin_names + load_plugins! + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins plugin_names, @initializer.loaded_plugins, failure_tip + end + + def test_registering_a_plugin_name_that_does_not_exist_raises_a_load_error + only_load_the_following_plugins! [:stubby, :acts_as_a_non_existant_plugin] + assert_raises(LoadError) do + load_plugins! + end + end + + def test_should_ensure_all_loaded_plugins_load_paths_are_added_to_the_load_path + only_load_the_following_plugins! [:stubby, :acts_as_chunky_bacon] + + @initializer.add_plugin_load_paths + + assert $LOAD_PATH.include?(File.join(plugin_fixture_path('default/stubby'), 'lib')) + assert $LOAD_PATH.include?(File.join(plugin_fixture_path('default/acts/acts_as_chunky_bacon'), 'lib')) + end + + private + + def load_plugins! + @initializer.add_plugin_load_paths + @initializer.load_plugins + end + end + +end diff --git a/railties/test/plugin_loader_test.rb b/railties/test/plugin_loader_test.rb index 3c2c37e7944fa..fe77de4474535 100644 --- a/railties/test/plugin_loader_test.rb +++ b/railties/test/plugin_loader_test.rb @@ -1,90 +1,140 @@ require File.dirname(__FILE__) + '/plugin_test_helper' -class TestPluginLoader < Test::Unit::TestCase - def setup - @initializer = Rails::Initializer.new(Rails::Configuration.new) - @valid_plugin_path = plugin_fixture_path('default/stubby') - @empty_plugin_path = plugin_fixture_path('default/empty') - end - - def test_determining_if_the_plugin_order_has_been_explicitly_set - loader = loader_for(@valid_plugin_path) - assert !loader.send(:explicit_plugin_loading_order?) - only_load_the_following_plugins! %w(stubby acts_as_chunky_bacon) - assert loader.send(:explicit_plugin_loading_order?) - end - - def test_enabled_if_not_named_explicitly - stubby_loader = loader_for(@valid_plugin_path) - acts_as_loader = loader_for('acts_as/acts_as_chunky_bacon') +uses_mocha "Plugin Loader Tests" do + + class TestPluginLoader < Test::Unit::TestCase + ORIGINAL_LOAD_PATH = $LOAD_PATH.dup - only_load_the_following_plugins! ['stubby', :all] - assert stubby_loader.send(:enabled?) - assert acts_as_loader.send(:enabled?) + def setup + reset_load_path! + + @configuration = Rails::Configuration.new + @configuration.plugin_paths << plugin_fixture_root_path + @initializer = Rails::Initializer.new(@configuration) + @valid_plugin_path = plugin_fixture_path('default/stubby') + @empty_plugin_path = plugin_fixture_path('default/empty') + + @loader = Rails::Plugin::Loader.new(@initializer) + end + + def test_should_locate_plugins_by_asking_each_locator_specifed_in_configuration_for_its_plugins_result + locator_1 = stub(:plugins => [:a, :b, :c]) + locator_2 = stub(:plugins => [:d, :e, :f]) + locator_class_1 = stub(:new => locator_1) + locator_class_2 = stub(:new => locator_2) + @configuration.plugin_locators = [locator_class_1, locator_class_2] + assert_equal [:a, :b, :c, :d, :e, :f], @loader.send(:locate_plugins) + end - assert stubby_loader.send(:explicitly_enabled?) - assert !acts_as_loader.send(:explicitly_enabled?) - end - - def test_determining_whether_a_given_plugin_is_loaded - plugin_loader = loader_for(@valid_plugin_path) - assert !plugin_loader.loaded? - assert_nothing_raised do - plugin_loader.send(:register_plugin_as_loaded) - end - assert plugin_loader.loaded? - end - - def test_if_a_path_is_a_plugin_path - # This is a plugin path, with a lib dir - assert loader_for(@valid_plugin_path).plugin_path? - # This just has an init.rb and no lib dir - assert loader_for(plugin_fixture_path('default/plugin_with_no_lib_dir')).plugin_path? - # This would be a plugin path, but the directory is empty - assert !loader_for(plugin_fixture_path('default/empty')).plugin_path? - # This is a non sense path - assert !loader_for(plugin_fixture_path('default/this_directory_does_not_exist')).plugin_path? - end - - def test_if_you_try_to_load_a_non_plugin_path_you_get_a_load_error - # This path is fine so nothing is raised - assert_nothing_raised do - loader_for(@valid_plugin_path).send(:report_nonexistant_or_empty_plugin!) + def test_should_memoize_the_result_of_locate_plugins_as_all_plugins + plugin_list = [:a, :b, :c] + @loader.expects(:locate_plugins).once.returns(plugin_list) + assert_equal plugin_list, @loader.all_plugins + assert_equal plugin_list, @loader.all_plugins # ensuring that locate_plugins isn't called again end - # This is an empty path so it raises - assert_raises(LoadError) do - loader_for(@empty_plugin_path).send(:report_nonexistant_or_empty_plugin!) + def test_should_return_empty_array_if_configuration_plugins_is_empty + @configuration.plugins = [] + assert_equal [], @loader.plugins end - assert_raises(LoadError) do - loader_for('this_is_not_a_plugin_directory').send(:report_nonexistant_or_empty_plugin!) + def test_should_find_all_availble_plugins_and_return_as_all_plugins + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:a, :acts_as_chunky_bacon, :plugin_with_no_lib_dir, :stubby], @loader.all_plugins, failure_tip end - end - - def test_loading_a_plugin_gives_the_init_file_access_to_all_it_needs - failure_tip = "Perhaps someone has written another test that loads this same plugin and therefore makes the StubbyMixin constant defined already." - assert !defined?(StubbyMixin), failure_tip - assert !added_to_load_path?(@valid_plugin_path) - # The init.rb of this plugin raises if it doesn't have access to all the things it needs - assert_nothing_raised do - loader_for(@valid_plugin_path).load - end - assert added_to_load_path?(@valid_plugin_path) - assert defined?(StubbyMixin) - end - - private - def loader_for(path, initializer = @initializer) - Rails::Plugin::Loader.new(initializer, path) + + def test_should_return_all_plugins_as_plugins_when_registered_plugin_list_is_untouched + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:a, :acts_as_chunky_bacon, :plugin_with_no_lib_dir, :stubby], @loader.plugins, failure_tip + end + + def test_should_return_all_plugins_as_plugins_when_registered_plugin_list_is_nil + @configuration.plugins = nil + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:a, :acts_as_chunky_bacon, :plugin_with_no_lib_dir, :stubby], @loader.plugins, failure_tip + end + + def test_should_return_specific_plugins_named_in_config_plugins_array_if_set + plugin_names = [:acts_as_chunky_bacon, :stubby] + only_load_the_following_plugins! plugin_names + assert_plugins plugin_names, @loader.plugins + end + + def test_should_respect_the_order_of_plugins_given_in_configuration + plugin_names = [:stubby, :acts_as_chunky_bacon] + only_load_the_following_plugins! plugin_names + assert_plugins plugin_names, @loader.plugins + end + + def test_should_load_all_plugins_in_natural_order_when_all_is_used + only_load_the_following_plugins! [:all] + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:a, :acts_as_chunky_bacon, :plugin_with_no_lib_dir, :stubby], @loader.plugins, failure_tip end - def plugin_fixture_path(path) - File.join(plugin_fixture_root_path, path) + def test_should_load_specified_plugins_in_order_and_then_all_remaining_plugins_when_all_is_used + only_load_the_following_plugins! [:stubby, :acts_as_chunky_bacon, :all] + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:stubby, :acts_as_chunky_bacon, :a, :plugin_with_no_lib_dir], @loader.plugins, failure_tip end - def added_to_load_path?(path) - $LOAD_PATH.grep(/#{path}/).size == 1 + def test_should_be_able_to_specify_loading_of_plugins_loaded_after_all + only_load_the_following_plugins! [:stubby, :all, :acts_as_chunky_bacon] + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:stubby, :a, :plugin_with_no_lib_dir, :acts_as_chunky_bacon], @loader.plugins, failure_tip + end + + def test_should_accept_plugin_names_given_as_strings + only_load_the_following_plugins! ['stubby', 'acts_as_chunky_bacon', :a, :plugin_with_no_lib_dir] + failure_tip = "It's likely someone has added a new plugin fixture without updating this list" + assert_plugins [:stubby, :acts_as_chunky_bacon, :a, :plugin_with_no_lib_dir], @loader.plugins, failure_tip + end + + def test_should_add_plugin_load_paths_to_global_LOAD_PATH_array + only_load_the_following_plugins! [:stubby, :acts_as_chunky_bacon] + stubbed_application_lib_index_in_LOAD_PATHS = 5 + @loader.stubs(:application_lib_index).returns(stubbed_application_lib_index_in_LOAD_PATHS) + + @loader.add_plugin_load_paths + + assert $LOAD_PATH.index(File.join(plugin_fixture_path('default/stubby'), 'lib')) >= stubbed_application_lib_index_in_LOAD_PATHS + assert $LOAD_PATH.index(File.join(plugin_fixture_path('default/acts/acts_as_chunky_bacon'), 'lib')) >= stubbed_application_lib_index_in_LOAD_PATHS + end + + def test_should_add_plugin_load_paths_to_Dependencies_load_paths + only_load_the_following_plugins! [:stubby, :acts_as_chunky_bacon] + + @loader.add_plugin_load_paths + + assert Dependencies.load_paths.include?(File.join(plugin_fixture_path('default/stubby'), 'lib')) + assert Dependencies.load_paths.include?(File.join(plugin_fixture_path('default/acts/acts_as_chunky_bacon'), 'lib')) end + def test_should_add_plugin_load_paths_to_Dependencies_load_once_paths + only_load_the_following_plugins! [:stubby, :acts_as_chunky_bacon] + + @loader.add_plugin_load_paths + + assert Dependencies.load_once_paths.include?(File.join(plugin_fixture_path('default/stubby'), 'lib')) + assert Dependencies.load_once_paths.include?(File.join(plugin_fixture_path('default/acts/acts_as_chunky_bacon'), 'lib')) + end + + def test_should_add_all_load_paths_from_a_plugin_to_LOAD_PATH_array + plugin_load_paths = ["a", "b"] + plugin = stub(:load_paths => plugin_load_paths) + @loader.stubs(:plugins).returns([plugin]) + + @loader.add_plugin_load_paths + + plugin_load_paths.each { |path| assert $LOAD_PATH.include?(path) } + end + + private + + def reset_load_path! + $LOAD_PATH.clear + ORIGINAL_LOAD_PATH.each { |path| $LOAD_PATH << path } + end + end + end \ No newline at end of file diff --git a/railties/test/plugin_locator_test.rb b/railties/test/plugin_locator_test.rb index 3ac4493d47bc6..ccd270dd10a0a 100644 --- a/railties/test/plugin_locator_test.rb +++ b/railties/test/plugin_locator_test.rb @@ -1,61 +1,69 @@ require File.dirname(__FILE__) + '/plugin_test_helper' -class TestPluginFileSystemLocator < Test::Unit::TestCase - def setup - configuration = Rails::Configuration.new - # We need to add our testing plugin directory to the plugin paths so - # the locator knows where to look for our plugins - configuration.plugin_paths << plugin_fixture_root_path - @initializer = Rails::Initializer.new(configuration) - @locator = new_locator - end +uses_mocha "Plugin Locator Tests" do + + class PluginLocatorTest < Test::Unit::TestCase - def test_no_plugins_are_loaded_if_the_configuration_has_an_empty_plugin_list - only_load_the_following_plugins! [] - assert_equal [], @locator.plugins - end + def test_should_require_subclasses_to_implement_the_plugins_method + assert_raises(RuntimeError) do + Rails::Plugin::Locator.new(nil).plugins + end + end - def test_only_the_specified_plugins_are_located_in_the_order_listed - plugin_names = [:stubby, :acts_as_chunky_bacon] - only_load_the_following_plugins! plugin_names - assert_equal plugin_names, @locator.plugin_names - end + def test_should_iterator_over_plugins_returned_by_plugins_when_calling_each + locator = Rails::Plugin::Locator.new(nil) + locator.stubs(:plugins).returns([:a, :b, :c]) + plugin_consumer = mock + plugin_consumer.expects(:consume).with(:a) + plugin_consumer.expects(:consume).with(:b) + plugin_consumer.expects(:consume).with(:c) + + locator.each do |plugin| + plugin_consumer.consume(plugin) + end + end - def test_all_plugins_are_loaded_when_registered_plugin_list_is_untouched - failure_tip = "It's likely someone has added a new plugin fixture without updating this list" - assert_equal [:a, :acts_as_chunky_bacon, :plugin_with_no_lib_dir, :stubby], @locator.plugin_names, failure_tip end + + + class PluginFileSystemLocatorTest < Test::Unit::TestCase + def setup + @configuration = Rails::Configuration.new + # We need to add our testing plugin directory to the plugin paths so + # the locator knows where to look for our plugins + @configuration.plugin_paths << plugin_fixture_root_path + @initializer = Rails::Initializer.new(@configuration) + @locator = Rails::Plugin::FileSystemLocator.new(@initializer) + @valid_plugin_path = plugin_fixture_path('default/stubby') + @empty_plugin_path = plugin_fixture_path('default/empty') + end + + def test_should_return_rails_plugin_instances_when_calling_create_plugin_with_a_valid_plugin_directory + assert_kind_of Rails::Plugin, @locator.send(:create_plugin, @valid_plugin_path) + end - def test_all_plugins_loaded_when_all_is_used - plugin_names = [:stubby, :acts_as_chunky_bacon, :all] - only_load_the_following_plugins! plugin_names - failure_tip = "It's likely someone has added a new plugin fixture without updating this list" - assert_equal [:stubby, :acts_as_chunky_bacon, :a, :plugin_with_no_lib_dir], @locator.plugin_names, failure_tip - end + def test_should_return_nil_when_calling_create_plugin_with_an_invalid_plugin_directory + assert_nil @locator.send(:create_plugin, @empty_plugin_path) + end - def test_all_plugins_loaded_after_all - plugin_names = [:stubby, :all, :acts_as_chunky_bacon] - only_load_the_following_plugins! plugin_names - failure_tip = "It's likely someone has added a new plugin fixture without updating this list" - assert_equal [:stubby, :a, :plugin_with_no_lib_dir, :acts_as_chunky_bacon], @locator.plugin_names, failure_tip - end + def test_should_return_all_plugins_found_under_the_set_plugin_paths + assert_equal ["a", "acts_as_chunky_bacon", "plugin_with_no_lib_dir", "stubby"], @locator.plugins.map(&:name) + end - def test_plugin_names_may_be_strings - plugin_names = ['stubby', 'acts_as_chunky_bacon', :a, :plugin_with_no_lib_dir] - only_load_the_following_plugins! plugin_names - failure_tip = "It's likely someone has added a new plugin fixture without updating this list" - assert_equal [:stubby, :acts_as_chunky_bacon, :a, :plugin_with_no_lib_dir], @locator.plugin_names, failure_tip - end + def test_should_find_plugins_only_under_the_plugin_paths_set_in_configuration + @configuration.plugin_paths = [File.join(plugin_fixture_root_path, "default")] + assert_equal ["acts_as_chunky_bacon", "plugin_with_no_lib_dir", "stubby"], @locator.plugins.map(&:name) + + @configuration.plugin_paths = [File.join(plugin_fixture_root_path, "alternate")] + assert_equal ["a"], @locator.plugins.map(&:name) + end - def test_registering_a_plugin_name_that_does_not_exist_raises_a_load_error - only_load_the_following_plugins! [:stubby, :acts_as_a_non_existant_plugin] - assert_raises(LoadError) do - @initializer.load_plugins + def test_should_not_raise_any_error_and_return_no_plugins_if_the_plugin_path_value_does_not_exist + @configuration.plugin_paths = ["some_missing_directory"] + assert_nothing_raised do + assert @locator.plugins.empty? + end end end - - private - def new_locator(initializer = @initializer) - Rails::Plugin::FileSystemLocator.new(initializer) - end -end \ No newline at end of file + +end # uses_mocha \ No newline at end of file diff --git a/railties/test/plugin_test.rb b/railties/test/plugin_test.rb new file mode 100644 index 0000000000000..a791e42ae8ba9 --- /dev/null +++ b/railties/test/plugin_test.rb @@ -0,0 +1,130 @@ +require File.dirname(__FILE__) + '/plugin_test_helper' + +uses_mocha "Plugin Tests" do + + class PluginTest < Test::Unit::TestCase + + def setup + @initializer = Rails::Initializer.new(Rails::Configuration.new) + @valid_plugin_path = plugin_fixture_path('default/stubby') + @empty_plugin_path = plugin_fixture_path('default/empty') + end + + def test_should_determine_plugin_name_from_the_directory_of_the_plugin + assert_equal 'stubby', plugin_for(@valid_plugin_path).name + assert_equal 'empty', plugin_for(@empty_plugin_path).name + end + + def test_should_not_be_loaded_when_created + assert !plugin_for(@valid_plugin_path).loaded? + end + + def test_should_be_marked_as_loaded_when_load_is_called + plugin = plugin_for(@valid_plugin_path) + assert !plugin.loaded? + plugin.stubs(:evaluate_init_rb) + assert_nothing_raised do + plugin.send(:load, anything) + end + assert plugin.loaded? + end + + def test_should_determine_validity_of_given_path + # This is a plugin path, with a lib dir + assert plugin_for(@valid_plugin_path).valid? + # This just has an init.rb and no lib dir + assert plugin_for(plugin_fixture_path('default/plugin_with_no_lib_dir')).valid? + # This would be a plugin path, but the directory is empty + assert !plugin_for(plugin_fixture_path('default/empty')).valid? + # This is a non sense path + assert !plugin_for(plugin_fixture_path('default/this_directory_does_not_exist')).valid? + end + + def test_should_return_empty_array_for_load_paths_when_plugin_has_no_lib_directory + assert_equal [], plugin_for(plugin_fixture_path('default/plugin_with_no_lib_dir')).load_paths + end + + def test_should_return_array_with_lib_path_for_load_paths_when_plugin_has_a_lib_directory + expected_lib_dir = File.join(plugin_fixture_path('default/stubby'), 'lib') + assert_equal [expected_lib_dir], plugin_for(plugin_fixture_path('default/stubby')).load_paths + end + + def test_should_raise_a_load_error_when_trying_to_determine_the_load_paths_from_an_invalid_plugin + assert_nothing_raised do + plugin_for(@valid_plugin_path).load_paths + end + + assert_raises(LoadError) do + plugin_for(@empty_plugin_path).load_paths + end + + assert_raises(LoadError) do + plugin_for('this_is_not_a_plugin_directory').load_paths + end + end + + def test_should_raise_a_load_error_when_trying_to_load_an_invalid_plugin + # This path is fine so nothing is raised + assert_nothing_raised do + plugin = plugin_for(@valid_plugin_path) + plugin.stubs(:evaluate_init_rb) + plugin.send(:load, @initializer) + end + + # This is an empty path so it raises + assert_raises(LoadError) do + plugin = plugin_for(@empty_plugin_path) + plugin.stubs(:evaluate_init_rb) + plugin.send(:load, @initializer) + end + + assert_raises(LoadError) do + plugin = plugin_for('this_is_not_a_plugin_directory') + plugin.stubs(:evaluate_init_rb) + plugin.send(:load, @initializer) + end + end + + def test_should_raise_a_load_error_when_trying_to_access_load_paths_of_an_invalid_plugin + # This path is fine so nothing is raised + assert_nothing_raised do + plugin_for(@valid_plugin_path).load_paths + end + + # This is an empty path so it raises + assert_raises(LoadError) do + plugin_for(@empty_plugin_path).load_paths + end + + assert_raises(LoadError) do + plugin_for('this_is_not_a_plugin_directory').load_paths + end + end + + def test_loading_a_plugin_gives_the_init_file_access_to_all_it_needs + failure_tip = "Perhaps someone has written another test that loads this same plugin and therefore makes the StubbyMixin constant defined already." + assert !defined?(StubbyMixin), failure_tip + plugin = plugin_for(@valid_plugin_path) + plugin.load_paths.each { |path| $LOAD_PATH.unshift(path) } + # The init.rb of this plugin raises if it doesn't have access to all the things it needs + assert_nothing_raised do + plugin.load(@initializer) + end + assert defined?(StubbyMixin) + end + + def test_should_sort_naturally_by_name + a = plugin_for("path/a") + b = plugin_for("path/b") + z = plugin_for("path/z") + assert_equal [a, b, z], [b, z, a].sort + end + + private + + def plugin_for(path) + Rails::Plugin.new(path) + end + end + +end # uses_mocha \ No newline at end of file diff --git a/railties/test/plugin_test_helper.rb b/railties/test/plugin_test_helper.rb index 0b065a5444948..f8c094d19f8cf 100644 --- a/railties/test/plugin_test_helper.rb +++ b/railties/test/plugin_test_helper.rb @@ -4,15 +4,26 @@ require 'test/unit' require 'active_support' require 'initializer' +require File.join(File.dirname(__FILE__), 'abstract_unit') # We need to set RAILS_ROOT if it isn't already set RAILS_ROOT = '.' unless defined?(RAILS_ROOT) + class Test::Unit::TestCase - def plugin_fixture_root_path - File.join(File.dirname(__FILE__), 'fixtures', 'plugins') - end + private + def plugin_fixture_root_path + File.join(File.dirname(__FILE__), 'fixtures', 'plugins') + end + + def only_load_the_following_plugins!(plugins) + @initializer.configuration.plugins = plugins + end - def only_load_the_following_plugins!(plugins) - @initializer.configuration.plugins = plugins - end + def plugin_fixture_path(path) + File.join(plugin_fixture_root_path, path) + end + + def assert_plugins(list_of_names, array_of_plugins, message=nil) + assert_equal list_of_names.map(&:to_s), array_of_plugins.map(&:name), message + end end \ No newline at end of file