diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2016-05-02 13:38:57 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2016-05-02 13:39:06 -0700 |
commit | 754889d5563a23ca55faa2534f951d8315b8b63e (patch) | |
tree | ed0557ecb311965b1c9cda036851f29d7fb62fe5 | |
parent | 34f0dfc8226860aa2d2bfcbf17dad6be20bea7e3 (diff) | |
download | chef-754889d5563a23ca55faa2534f951d8315b8b63e.tar.gz |
WIP
-rw-r--r-- | lib/chef/decorator.rb | 81 | ||||
-rw-r--r-- | lib/chef/decorator/lazy.rb | 50 | ||||
-rw-r--r-- | lib/chef/decorator/lazy_array.rb | 44 | ||||
-rw-r--r-- | lib/chef/provider/package.rb | 31 | ||||
-rw-r--r-- | lib/chef/provider/package/dpkg.rb | 4 | ||||
-rw-r--r-- | spec/unit/decorator/lazy_array_spec.rb | 58 | ||||
-rw-r--r-- | spec/unit/decorator/lazy_spec.rb | 39 | ||||
-rw-r--r-- | spec/unit/decorator_spec.rb | 142 |
8 files changed, 421 insertions, 28 deletions
diff --git a/lib/chef/decorator.rb b/lib/chef/decorator.rb new file mode 100644 index 0000000000..aa1f399d9f --- /dev/null +++ b/lib/chef/decorator.rb @@ -0,0 +1,81 @@ +#-- +# Copyright:: Copyright 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 "delegate" + +class Chef + class Decorator < SimpleDelegator + NULL = ::Object.new + + def initialize(obj: NULL) + super(obj) unless obj == NULL + @__defined_methods__ = [] + end + + # if we wrap a nil then decorator.nil? should be true + def nil? + __getobj__.nil? + end + + # if we wrap a Hash then decorator.is_a?(Hash) should be true + def is_a?(klass) + __getobj__.is_a?(klass) || super + end + + # if we wrap a Hash then decorator.kind_of?(Hash) should be true + def kind_of?(klass) + __getobj__.kind_of?(klass) || super + end + + # reset our methods on the instance if the object changes under us (this also + # clears out the closure over the target we create in method_missing below) + def __setobj__(obj) + __reset_methods__ + super + end + + # this is the ruby 2.2/2.3 implementation of Delegator#method_missing() with + # adding the define_singleton_method call and @__defined_methods__ tracking + def method_missing(m, *args, &block) + r = true + target = self.__getobj__ { r = false } + + if r && target.respond_to?(m) + # these next 4 lines are the patched code + define_singleton_method(m) do |*args, &block| + target.__send__(m, *args, &block) + end + @__defined_methods__.push(m) + target.__send__(m, *args, &block) + elsif ::Kernel.respond_to?(m, true) + ::Kernel.instance_method(m).bind(self).call(*args, &block) + else + super(m, *args, &block) + end + end + + private + + # used by __setobj__ to clear the methods we've built on the instance. + def __reset_methods__ + @__defined_methods__.each do |m| + singleton_class.send(:undef_method, m) + end + @__defined_methods__ = [] + end + end +end diff --git a/lib/chef/decorator/lazy.rb b/lib/chef/decorator/lazy.rb new file mode 100644 index 0000000000..067d6bd7f8 --- /dev/null +++ b/lib/chef/decorator/lazy.rb @@ -0,0 +1,50 @@ +#-- +# Copyright:: Copyright 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/decorator" + +class Chef + class Decorator + # Lazy wrapper to delay construction of an object until a method is + # called against the object. + # + # @example + # a = Chef::Decorator::Lazy.new { puts "allocated" } + # puts "start" + # puts a.class + # + # outputs: + # + # start + # allocated + # String + # + # @since 12.10.x + class Lazy < Decorator + def initialize(&block) + super + @block = block + end + + def __getobj__ + __setobj__(@block.call) unless defined?(@delegate_sd_obj) + super + end + + end + end +end diff --git a/lib/chef/decorator/lazy_array.rb b/lib/chef/decorator/lazy_array.rb new file mode 100644 index 0000000000..3879aea5f6 --- /dev/null +++ b/lib/chef/decorator/lazy_array.rb @@ -0,0 +1,44 @@ +#-- +# Copyright:: Copyright 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/decorator" + +class Chef + class Decorator + # Lazy wrapper to delay construction of an object until a method is + # called against the object. + # + # @example + # a = Chef::Decorator::Lazy.new { puts "allocated" } + # puts "start" + # puts a.class + # + # outputs: + # + # start + # allocated + # String + # + # @since 12.10.x + class LazyArray < Lazy + def [](idx) + block = @block + Lazy.new { block.call[idx] } + end + end + end +end diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index 4a37ef533d..ec71832488 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -22,6 +22,8 @@ require "chef/mixin/subclass_directive" require "chef/log" require "chef/file_cache" require "chef/platform" +require "chef/decorator/lazy" +require "chef/decorator/lazy_array" class Chef class Provider @@ -476,32 +478,9 @@ class Chef # @return [Array] candidate_version(s) as an array def candidate_version_array - use_multipackage_api? ? - [ candidate_version ].flatten : - [ LazyObject.new { candidate_version } ] - end - - # Wrap single candidate_version in a lazy object to minimize unnecessary API queries. - class LazyObject < BasicObject - NULL = ::Object.new - - def initialize(&block) - @block = block - @target = NULL - end - - def __obj - @target = @block.call if @target == NULL - @target - end - - def ==(other_object) - __obj == other_object - end - - def method_missing(method_name, *args, &block) - __obj.send(method_name, *args, &block) - end + # NOTE: even with use_multipackage_api candidate_version may be a bare nil and need wrapping + # ( looking at you, dpkg provider... ) + Chef::Decorator::LazyArray.new { [ candidate_version ].flatten } end # @return [Array] current_version(s) as an array diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index a5a80e14d6..e57f58e052 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -23,9 +23,9 @@ class Chef class Provider class Package class Dpkg < Chef::Provider::Package - DPKG_REMOVED = /^Status: deinstall ok config-files/ + DPKG_REMOVED = /^Status: deinstall ok config-files/ DPKG_INSTALLED = /^Status: install ok installed/ - DPKG_VERSION = /^Version: (.+)$/ + DPKG_VERSION = /^Version: (.+)$/ provides :dpkg_package, os: "linux" diff --git a/spec/unit/decorator/lazy_array_spec.rb b/spec/unit/decorator/lazy_array_spec.rb new file mode 100644 index 0000000000..0c5c2eeee0 --- /dev/null +++ b/spec/unit/decorator/lazy_array_spec.rb @@ -0,0 +1,58 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# 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" + +describe Chef::Decorator::LazyArray do + def foo + @foo ||= 1 + end + + def bar + @bar ||= 2 + end + + let(:decorator) do + Chef::Decorator::LazyArray.new { [ foo, bar ] } + end + + it "behaves like an array" do + expect(decorator[0]).to eql(1) + expect(decorator[1]).to eql(2) + end + + it "accessing the array elements is lazy" do + expect(decorator[0].class).to eql(Chef::Decorator::Lazy) + expect(decorator[1].class).to eql(Chef::Decorator::Lazy) + expect(@foo).to be nil + expect(@bar).to be nil + end + + it "calling a method on the array element runs the proc (and both elements are autovivified)" do + expect(decorator[0].nil?).to be false + expect(@foo).to equal(1) + expect(@bar).to equal(2) + end + + it "if we loop over the elements and do nothing then its not lazy" do + # we don't know how many elements there are unless we evaluate the proc + decorator.each { |i| } + expect(@foo).to equal(1) + expect(@bar).to equal(2) + end +end diff --git a/spec/unit/decorator/lazy_spec.rb b/spec/unit/decorator/lazy_spec.rb new file mode 100644 index 0000000000..4ea8301b63 --- /dev/null +++ b/spec/unit/decorator/lazy_spec.rb @@ -0,0 +1,39 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# 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" + +describe Chef::Decorator::Lazy do + let(:decorator) do + @a = 0 + Chef::Decorator::Lazy.new { @a = @a + 1 } + end + + it "decorates an object" do + expect(decorator.even?).to be false + end + + it "the proc runs and does work" do + expect(decorator).to eql(1) + end + + it "creating the decorator does not cause the proc to run" do + decorator + expect(@a).to eql(0) + end +end diff --git a/spec/unit/decorator_spec.rb b/spec/unit/decorator_spec.rb new file mode 100644 index 0000000000..6d73db2cc4 --- /dev/null +++ b/spec/unit/decorator_spec.rb @@ -0,0 +1,142 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# 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" + +def impersonates_a(klass) + it "#is_a?(#{klass}) is true" do + expect(decorator.is_a?(klass)).to be true + end + + it "#is_a?(Chef::Decorator) is true" do + expect(decorator.is_a?(Chef::Decorator)).to be true + end + + it "#kind_of?(#{klass}) is true" do + expect(decorator.kind_of?(klass)).to be true + end + + it "#kind_of?(Chef::Decorator) is true" do + expect(decorator.kind_of?(Chef::Decorator)).to be true + end + + it "#instance_of?(#{klass}) is false" do + expect(decorator.instance_of?(klass)).to be false + end + + it "#instance_of?(Chef::Decorator) is true" do + expect(decorator.instance_of?(Chef::Decorator)).to be true + end + + it "#class is Chef::Decorator" do + expect(decorator.class).to eql(Chef::Decorator) + end +end + +describe Chef::Decorator do + let(:obj) {} + let(:decorator) { Chef::Decorator.new(obj) } + + context "when the obj is a string" do + let(:obj) { "STRING" } + + impersonates_a(String) + + it "#nil? is false" do + expect(decorator.nil?).to be false + end + + it "!! is true" do + expect(!!decorator).to be true + end + + it "dup returns a decorator" do + expect(decorator.dup.class).to be Chef::Decorator + end + + it "dup dup's the underlying thing" do + expect(decorator.dup.__getobj__).not_to equal(decorator.__getobj__) + end + end + + context "when the obj is a nil" do + let(:obj) { nil } + + it "#nil? is true" do + expect(decorator.nil?).to be true + end + + it "!! is false" do + expect(!!decorator).to be false + end + + impersonates_a(NilClass) + end + + context "when the obj is an empty Hash" do + let(:obj) { {} } + + impersonates_a(Hash) + + it "formats it correctly through ffi-yajl and not the JSON gem" do + # this relies on a quirk of pretty formatting whitespace between yajl and ruby's JSON + expect(FFI_Yajl::Encoder.encode(decorator, pretty: true)).to eql("{\n\n}\n") + end + end + + context "whent he obj is a Hash with elements" do + let(:obj) { { foo: "bar", baz: "qux" } } + + impersonates_a(Hash) + + it "dup is shallow on the Hash" do + expect(decorator.dup[:baz]).to equal(decorator[:baz]) + end + + it "deep mutating the dup'd hash mutates the origin" do + decorator.dup[:baz] << "qux" + expect(decorator[:baz]).to eql("quxqux") + end + end + + context "memoizing methods" do + let(:obj) { {} } + + it "calls method_missing only once" do + expect(decorator).to receive(:method_missing).once.and_call_original + expect(decorator.keys).to eql([]) + expect(decorator.keys).to eql([]) + end + + it "switching a Hash to an Array responds to keys then does not" do + expect(decorator.respond_to?(:keys)).to be true + expect(decorator.keys).to eql([]) + decorator.__setobj__([]) + expect(decorator.respond_to?(:keys)).to be false + expect { decorator.keys }.to raise_error(NoMethodError) + end + + it "memoization of methods happens on the instances, not the classes" do + decorator2 = Chef::Decorator.new([]) + expect(decorator.respond_to?(:keys)).to be true + expect(decorator.keys).to eql([]) + expect(decorator2.respond_to?(:keys)).to be false + expect { decorator2.keys }.to raise_error(NoMethodError) + end + end +end |