summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2016-05-02 13:38:57 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2016-05-02 13:39:06 -0700
commit754889d5563a23ca55faa2534f951d8315b8b63e (patch)
treeed0557ecb311965b1c9cda036851f29d7fb62fe5
parent34f0dfc8226860aa2d2bfcbf17dad6be20bea7e3 (diff)
downloadchef-754889d5563a23ca55faa2534f951d8315b8b63e.tar.gz
WIP
-rw-r--r--lib/chef/decorator.rb81
-rw-r--r--lib/chef/decorator/lazy.rb50
-rw-r--r--lib/chef/decorator/lazy_array.rb44
-rw-r--r--lib/chef/provider/package.rb31
-rw-r--r--lib/chef/provider/package/dpkg.rb4
-rw-r--r--spec/unit/decorator/lazy_array_spec.rb58
-rw-r--r--spec/unit/decorator/lazy_spec.rb39
-rw-r--r--spec/unit/decorator_spec.rb142
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