From 856baec7f9059e017620bbe65677a02f0ae4d7c2 Mon Sep 17 00:00:00 2001
From: Takashi Kato <tohosaku@users.osdn.me>
Date: Mon, 25 Apr 2023 07:16:39 +0900
Subject: [PATCH] Defer plugin requirements evaluation

---
 lib/redmine/plugin.rb                            | 16 +++++++++++++++-
 lib/redmine/plugin_loader.rb                     |  7 ++++++-
 .../plugin_dependencies/a_plugin/init.rb         |  6 ++++++
 .../plugin_dependencies/b_plugin/init.rb         |  6 ++++++
 .../plugin_dependencies/c_plugin/init.rb         |  6 ++++++
 .../plugin_dependencies/d_plugin/init.rb         |  5 +++++
 test/unit/lib/redmine/plugin_test.rb             | 16 ++++++++++++++++
 7 files changed, 60 insertions(+), 2 deletions(-)
 create mode 100644 test/fixtures/plugin_dependencies/a_plugin/init.rb
 create mode 100644 test/fixtures/plugin_dependencies/b_plugin/init.rb
 create mode 100644 test/fixtures/plugin_dependencies/c_plugin/init.rb
 create mode 100644 test/fixtures/plugin_dependencies/d_plugin/init.rb

diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb
index b7f243354..f1e64740a 100644
--- a/lib/redmine/plugin.rb
+++ b/lib/redmine/plugin.rb
@@ -78,7 +78,7 @@ module Redmine
       end
     end
     def_field :name, :description, :url, :author, :author_url, :version, :settings, :directory
-    attr_reader :id
+    attr_reader :id, :requirements
 
     # Plugin constructor: instanciates a new Redmine::Plugin with given +id+
     # and make it evaluate the given +block+
@@ -169,8 +169,13 @@ module Redmine
       registered_plugins[id.to_sym].present?
     end
 
+    def self.validate
+      registered_plugins.each_value(&:validate)
+    end
+
     def initialize(id)
       @id = id.to_sym
+      @requirements = {}
     end
 
     def public_directory
@@ -269,6 +274,10 @@ module Redmine
     #   requires_redmine_plugin :foo, :version => '0.7.3'              # 0.7.3 only
     #   requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0']   # 0.7.3 or 0.8.0
     def requires_redmine_plugin(plugin_name, arg)
+      @requirements[plugin_name] = ->{ validate_plugin_requirement(plugin_name, arg) }
+    end
+
+    def validate_plugin_requirement(plugin_name, arg)
       arg = {:version_or_higher => arg} unless arg.is_a?(Hash)
       arg.assert_valid_keys(:version, :version_or_higher)
 
@@ -305,6 +314,11 @@ module Redmine
       end
       true
     end
+    private :validate_plugin_requirement
+
+    def validate
+      requirements.each_value(&:call)
+    end
 
     # Adds an item to the given +menu+.
     # The +id+ parameter (equals to the project id) is automatically added to the url.
diff --git a/lib/redmine/plugin_loader.rb b/lib/redmine/plugin_loader.rb
index 2151e949b..cb5d6aa40 100644
--- a/lib/redmine/plugin_loader.rb
+++ b/lib/redmine/plugin_loader.rb
@@ -105,12 +105,17 @@ module Redmine
       add_autoload_paths
 
       Rails.application.config.to_prepare do
-        PluginLoader.directories.each(&:run_initializer)
+        Redmine::PluginLoader.run_initializer
+        Redmine::Plugin.validate
 
         Redmine::Hook.call_hook :after_plugins_loaded
       end
     end
 
+    def self.run_initializer
+      directories.each(&:run_initializer)
+    end
+
     def self.setup
       @plugin_directories = []
 
diff --git a/test/fixtures/plugin_dependencies/a_plugin/init.rb b/test/fixtures/plugin_dependencies/a_plugin/init.rb
new file mode 100644
index 000000000..0b4856460
--- /dev/null
+++ b/test/fixtures/plugin_dependencies/a_plugin/init.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Redmine::Plugin.register :a_plugin do
+  requires_redmine_plugin :d_plugin, '0.1.0'
+  version '0.1.0'
+end
diff --git a/test/fixtures/plugin_dependencies/b_plugin/init.rb b/test/fixtures/plugin_dependencies/b_plugin/init.rb
new file mode 100644
index 000000000..dfd3183d6
--- /dev/null
+++ b/test/fixtures/plugin_dependencies/b_plugin/init.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Redmine::Plugin.register :b_plugin do
+  requires_redmine_plugin :d_plugin, '0.1.0'
+  version '0.1.0'
+end
diff --git a/test/fixtures/plugin_dependencies/c_plugin/init.rb b/test/fixtures/plugin_dependencies/c_plugin/init.rb
new file mode 100644
index 000000000..8cc7a6b07
--- /dev/null
+++ b/test/fixtures/plugin_dependencies/c_plugin/init.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Redmine::Plugin.register :c_plugin do
+  requires_redmine_plugin :a_plugin, '0.1.0'
+  requires_redmine_plugin :b_plugin, '0.1.0'
+end
diff --git a/test/fixtures/plugin_dependencies/d_plugin/init.rb b/test/fixtures/plugin_dependencies/d_plugin/init.rb
new file mode 100644
index 000000000..04d539bdc
--- /dev/null
+++ b/test/fixtures/plugin_dependencies/d_plugin/init.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Redmine::Plugin.register :d_plugin do
+  version '0.1.0'
+end
diff --git a/test/unit/lib/redmine/plugin_test.rb b/test/unit/lib/redmine/plugin_test.rb
index 02df23236..6ddc6cee8 100644
--- a/test/unit/lib/redmine/plugin_test.rb
+++ b/test/unit/lib/redmine/plugin_test.rb
@@ -174,28 +174,44 @@ class Redmine::PluginTest < ActiveSupport::TestCase
       test.assert requires_redmine_plugin(:other_plugin, other_version)
       test.assert_raise Redmine::PluginRequirementError do
         requires_redmine_plugin(:other_plugin, :version_or_higher => '99.0.0')
+        validate
       end
       test.assert requires_redmine_plugin(:other_plugin, :version => other_version)
       test.assert requires_redmine_plugin(:other_plugin, :version => [other_version, '99.0.0'])
       test.assert_raise Redmine::PluginRequirementError do
         requires_redmine_plugin(:other_plugin, :version => '99.0.0')
+        validate
       end
       test.assert_raise Redmine::PluginRequirementError do
         requires_redmine_plugin(:other_plugin, :version => ['98.0.0', '99.0.0'])
+        validate
       end
       # Missing plugin
       test.assert_raise Redmine::PluginRequirementError do
         requires_redmine_plugin(:missing, :version_or_higher => '0.1.0')
+        validate
       end
       test.assert_raise Redmine::PluginRequirementError do
         requires_redmine_plugin(:missing, '0.1.0')
+        validate
       end
       test.assert_raise Redmine::PluginRequirementError do
         requires_redmine_plugin(:missing, :version => '0.1.0')
+        validate
       end
     end
   end
 
+  def test_requires_redmine_plugin_dependencies
+    @klass.clear
+    @klass.directory = Rails.root.join('test/fixtures/plugin_dependencies')
+    Redmine::PluginLoader.directory = @klass.directory
+    Redmine::PluginLoader.setup
+    Redmine::PluginLoader.run_initializer
+    Redmine::Plugin.validate
+    assert_equal 4, Redmine::Plugin.registered_plugins.keys.size
+  end
+
   def test_default_settings
     @klass.register(:foo_plugin) {settings :default => {'key1' => 'abc', :key2 => 123}}
     h = Setting.plugin_foo_plugin
-- 
2.30.2

