From d32484b767e11d4da48fd1c185315fe08b25fdbe Mon Sep 17 00:00:00 2001 From: Lamont Granquist Date: Tue, 15 Mar 2016 15:09:45 -0700 Subject: lazy module inclusion into DSL modules Chef::DSL::Recipe::FullDSL.send(:include, MyModule) will now patch all its descendants that it has been included into (works the way actual inheritance works now). --- lib/chef/dsl/recipe.rb | 9 +++- lib/chef/mixin/lazy_module_include.rb | 77 ++++++++++++++++++++++++++++++++++ lib/chef/recipe.rb | 1 - spec/unit/dsl/recipe_spec.rb | 13 ++++++ spec/unit/dsl/registry_helper_spec.rb | 52 +++++++++++++++++++++++ spec/unit/dsl/regsitry_helper_spec.rb | 52 ----------------------- spec/unit/lwrp_spec.rb | 58 +++++++++++++++++++++++++ spec/unit/mixin/lazy_module_include.rb | 71 +++++++++++++++++++++++++++++++ 8 files changed, 278 insertions(+), 55 deletions(-) create mode 100644 lib/chef/mixin/lazy_module_include.rb create mode 100644 spec/unit/dsl/registry_helper_spec.rb delete mode 100644 spec/unit/dsl/regsitry_helper_spec.rb create mode 100644 spec/unit/mixin/lazy_module_include.rb diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index 6d254df48d..22be303c5b 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -23,21 +23,21 @@ require "chef/mixin/powershell_out" require "chef/dsl/resources" require "chef/dsl/definitions" require "chef/dsl/declare_resource" +require "chef/mixin/lazy_module_include" class Chef module DSL - # == Chef::DSL::Recipe # Provides the primary recipe DSL functionality for defining Chef resource # objects via method calls. module Recipe - include Chef::Mixin::ShellOut include Chef::Mixin::PowershellOut include Chef::DSL::Resources include Chef::DSL::Definitions include Chef::DSL::DeclareResource + extend Chef::Mixin::LazyModuleInclude def resource_class_for(snake_case_name) Chef::Resource.resource_for_node(snake_case_name, run_context.node) @@ -113,6 +113,8 @@ class Chef require "chef/dsl/reboot_pending" require "chef/dsl/audit" require "chef/dsl/powershell" + require "chef/mixin/lazy_module_include" + include Chef::DSL::DataQuery include Chef::DSL::PlatformIntrospection include Chef::DSL::IncludeRecipe @@ -121,6 +123,9 @@ class Chef include Chef::DSL::RebootPending include Chef::DSL::Audit include Chef::DSL::Powershell + + extend Chef::Mixin::LazyModuleInclude + end end end diff --git a/lib/chef/mixin/lazy_module_include.rb b/lib/chef/mixin/lazy_module_include.rb new file mode 100644 index 0000000000..34e1fce4f1 --- /dev/null +++ b/lib/chef/mixin/lazy_module_include.rb @@ -0,0 +1,77 @@ +# +# Copyright:: Copyright 2011-2016, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + # If you have: + # + # module A + # extend LazyModuleInclude + # end + # + # module B + # include A + # end + # + # module C + # include B + # end + # + # module Monkeypatches + # def monkey + # puts "monkey!" + # end + # end + # + # A.send(:include, Monkeypatches) + # + # Then B and C and any classes that they're included in will also get the #monkey method patched into them. + # + module LazyModuleInclude + + # Most of the magick is in this hook which creates a closure over the parent class and then builds an + # "infector" module which infects all descendants and which is responsible for updating the list of + # descendants in the parent class. + def included(klass) + super + parent_klass = self + infector = Module.new do + define_method(:included) do |subklass| + super(subklass) + subklass.extend(infector) + parent_klass.descendants.push(subklass) + end + end + klass.extend(infector) + parent_klass.descendants.push(klass) + end + + def descendants + @descendants ||= [] + end + + def include(*classes) + super + classes.each do |klass| + descendants.each do |descendant| + descendant.send(:include, klass) + end + end + end + end + end +end diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index 3a91781b2e..55b6fd7d52 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -114,6 +114,5 @@ class Chef run_context.node.tags.delete(tag) end end - end end diff --git a/spec/unit/dsl/recipe_spec.rb b/spec/unit/dsl/recipe_spec.rb index cd01079c15..bc97ecc029 100644 --- a/spec/unit/dsl/recipe_spec.rb +++ b/spec/unit/dsl/recipe_spec.rb @@ -24,6 +24,11 @@ class RecipeDSLExampleClass include Chef::DSL::Recipe end +FullRecipeDSLExampleClass = Struct.new(:cookbook_name, :recipe_name) +class FullRecipeDSLExampleClass + include Chef::DSL::Recipe::FullDSL +end + RecipeDSLBaseAPI = Struct.new(:cookbook_name, :recipe_name) class RecipeDSLExampleSubclass < RecipeDSLBaseAPI include Chef::DSL::Recipe @@ -36,6 +41,14 @@ describe Chef::DSL::Recipe do let(:cookbook_name) { "example_cb" } let(:recipe_name) { "example_recipe" } + it "tracks when it is included via FullDSL" do + expect(Chef::DSL::Recipe::FullDSL.descendants).to include(FullRecipeDSLExampleClass) + end + + it "doesn't track what is included via only the recipe DSL" do + expect(Chef::DSL::Recipe::FullDSL.descendants).not_to include(RecipeDSLExampleClass) + end + shared_examples_for "A Recipe DSL Implementation" do it "responds to cookbook_name" do diff --git a/spec/unit/dsl/registry_helper_spec.rb b/spec/unit/dsl/registry_helper_spec.rb new file mode 100644 index 0000000000..45c7e73979 --- /dev/null +++ b/spec/unit/dsl/registry_helper_spec.rb @@ -0,0 +1,52 @@ +# +# Author:: Prajakta Purohit () +# Copyright:: Copyright 2011-2016, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/dsl/registry_helper" +require "spec_helper" + +describe Chef::Resource::RegistryKey do + + before (:all) do + events = Chef::EventDispatch::Dispatcher.new + node = Chef::Node.new + node.consume_external_attrs(OHAI_SYSTEM.data, {}) + run_context = Chef::RunContext.new(node, {}, events) + @resource = Chef::Resource.new("foo", run_context) + end + + context "tests registry dsl" do + it "resource can access registry_helper method registry_key_exists" do + expect(@resource.respond_to?("registry_key_exists?")).to eq(true) + end + it "resource can access registry_helper method registry_get_values" do + expect(@resource.respond_to?("registry_get_values")).to eq(true) + end + it "resource can access registry_helper method registry_has_subkey" do + expect(@resource.respond_to?("registry_has_subkeys?")).to eq(true) + end + it "resource can access registry_helper method registry_get_subkeys" do + expect(@resource.respond_to?("registry_get_subkeys")).to eq(true) + end + it "resource can access registry_helper method registry_value_exists" do + expect(@resource.respond_to?("registry_value_exists?")).to eq(true) + end + it "resource can access registry_helper method data_value_exists" do + expect(@resource.respond_to?("registry_data_exists?")).to eq(true) + end + end +end diff --git a/spec/unit/dsl/regsitry_helper_spec.rb b/spec/unit/dsl/regsitry_helper_spec.rb deleted file mode 100644 index 45c7e73979..0000000000 --- a/spec/unit/dsl/regsitry_helper_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# -# Author:: Prajakta Purohit () -# Copyright:: Copyright 2011-2016, Chef Software Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "chef/dsl/registry_helper" -require "spec_helper" - -describe Chef::Resource::RegistryKey do - - before (:all) do - events = Chef::EventDispatch::Dispatcher.new - node = Chef::Node.new - node.consume_external_attrs(OHAI_SYSTEM.data, {}) - run_context = Chef::RunContext.new(node, {}, events) - @resource = Chef::Resource.new("foo", run_context) - end - - context "tests registry dsl" do - it "resource can access registry_helper method registry_key_exists" do - expect(@resource.respond_to?("registry_key_exists?")).to eq(true) - end - it "resource can access registry_helper method registry_get_values" do - expect(@resource.respond_to?("registry_get_values")).to eq(true) - end - it "resource can access registry_helper method registry_has_subkey" do - expect(@resource.respond_to?("registry_has_subkeys?")).to eq(true) - end - it "resource can access registry_helper method registry_get_subkeys" do - expect(@resource.respond_to?("registry_get_subkeys")).to eq(true) - end - it "resource can access registry_helper method registry_value_exists" do - expect(@resource.respond_to?("registry_value_exists?")).to eq(true) - end - it "resource can access registry_helper method data_value_exists" do - expect(@resource.respond_to?("registry_data_exists?")).to eq(true) - end - end -end diff --git a/spec/unit/lwrp_spec.rb b/spec/unit/lwrp_spec.rb index 5afd838551..937915055e 100644 --- a/spec/unit/lwrp_spec.rb +++ b/spec/unit/lwrp_spec.rb @@ -717,4 +717,62 @@ describe "LWRP" do end end end + + describe "extending the DSL mixin" do + module MyAwesomeDSLExensionClass + def my_awesome_dsl_extension(argument) + argument + end + end + + class MyAwesomeResource < Chef::Resource::LWRPBase + provides :my_awesome_resource + resource_name :my_awesome_resource + default_action :create + end + + class MyAwesomeProvider < Chef::Provider::LWRPBase + use_inline_resources + + provides :my_awesome_resource + + action :create do + my_awesome_dsl_extension("foo") + end + end + + let(:recipe) { + cookbook_repo = File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "cookbooks")) + cookbook_loader = Chef::CookbookLoader.new(cookbook_repo) + cookbook_loader.load_cookbooks + cookbook_collection = Chef::CookbookCollection.new(cookbook_loader) + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, cookbook_collection, events) + Chef::Recipe.new("hjk", "test", run_context) + } + + it "lets you extend the recipe DSL" do + expect(Chef::Recipe).to receive(:include).with(MyAwesomeDSLExensionClass) + expect(Chef::Provider::InlineResources).to receive(:include).with(MyAwesomeDSLExensionClass) + Chef::DSL::Recipe::FullDSL.send(:include, MyAwesomeDSLExensionClass) + end + + it "lets you call your DSL from a recipe" do + Chef::DSL::Recipe::FullDSL.send(:include, MyAwesomeDSLExensionClass) + expect(recipe.my_awesome_dsl_extension("foo")).to eql("foo") + end + + it "lets you call your DSL from a provider" do + Chef::DSL::Recipe::FullDSL.send(:include, MyAwesomeDSLExensionClass) + + resource = MyAwesomeResource.new("name", run_context) + run_context.resource_collection << resource + + runner = Chef::Runner.new(run_context) + expect_any_instance_of(MyAwesomeProvider).to receive(:my_awesome_dsl_extension).and_call_original + runner.converge + end + end + end diff --git a/spec/unit/mixin/lazy_module_include.rb b/spec/unit/mixin/lazy_module_include.rb new file mode 100644 index 0000000000..542ae853ae --- /dev/null +++ b/spec/unit/mixin/lazy_module_include.rb @@ -0,0 +1,71 @@ +# +# Copyright:: Copyright 2015-2016, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" + +module TestA + extend Chef::Mixin::LazyModuleInclude +end + +module TestB + include TestA + extend Chef::Mixin::LazyModuleInclude +end + +class TestC + include TestB +end + +module Monkey + def monkey + "monkey" + end +end + +module Klowns + def klowns + "klowns" + end +end + +TestA.send(:include, Monkey) + +TestB.send(:include, Klowns) + +describe Chef::Mixin::LazyModuleInclude do + + it "tracks descendant classes of TestA" do + expect(TestA.descendants).to include(TestB) + expect(TestA.descendants).to include(TestC) + end + + it "tracks descendent classes of TestB" do + expect(TestB.descendants).to eql([TestC]) + end + + it "including into A mixins in methods into B and C" do + expect(TestA.instance_methods).to include(:monkey) + expect(TestB.instance_methods).to include(:monkey) + expect(TestC.instance_methods).to include(:monkey) + end + + it "including into B only mixins in methods into C" do + expect(TestA.instance_methods).not_to include(:klowns) + expect(TestB.instance_methods).to include(:klowns) + expect(TestC.instance_methods).to include(:klowns) + end +end -- cgit v1.2.1