[Pkg-puppet-devel] [facter] 172/352: (FACT-237) Add aggregate resolution class
Stig Sandbeck Mathisen
ssm at debian.org
Sun Apr 6 22:21:43 UTC 2014
This is an automated email from the git hooks/post-receive script.
ssm pushed a commit to branch master
in repository facter.
commit b5c42931725aa2f6f1b4c6531ed006d29f7ab62e
Author: Adrien Thebo <git at somethingsinistral.net>
Date: Tue Jan 7 13:59:38 2014 -0800
(FACT-237) Add aggregate resolution class
---
lib/facter/core/aggregate.rb | 185 +++++++++++++++++++++++++++++++++++++++
lib/facter/core/resolvable.rb | 1 -
spec/unit/core/aggregate_spec.rb | 130 +++++++++++++++++++++++++++
3 files changed, 315 insertions(+), 1 deletion(-)
diff --git a/lib/facter/core/aggregate.rb b/lib/facter/core/aggregate.rb
new file mode 100644
index 0000000..20b51cd
--- /dev/null
+++ b/lib/facter/core/aggregate.rb
@@ -0,0 +1,185 @@
+require 'facter'
+require 'facter/core/directed_graph'
+require 'facter/core/suitable'
+require 'facter/core/resolvable'
+
+# Aggregates provide a mechanism for facts to be resolved in multiple steps.
+#
+# Aggregates are evaluated in two parts: generating individual chunks and then
+# aggregating all chunks together. Each chunk is a block of code that generates
+# a value, and may depend on other chunks when it runs. After all chunks have
+# been evaluated they are passed to the aggregate block as Hash<name, result>.
+# The aggregate block converts the individual chunks into a single value that is
+# returned as the final value of the aggregate.
+#
+# @api public
+# @since 2.0.0
+class Facter::Core::Aggregate
+
+ include Facter::Core::Suitable
+ include Facter::Core::Resolvable
+
+ # @!attribute [r] name
+ # @return [Symbol] The name of the aggregate resolution
+ attr_reader :name
+
+ # @!attribute [r] deps
+ # @api private
+ # @return [Facter::Core::DirectedGraph]
+ attr_reader :deps
+
+ # @!attribute [r] confines
+ # @return [Array<Facter::Core::Confine>] An array of confines restricting
+ # this to a specific platform
+ # @see Facter::Core::Suitable
+ attr_reader :confines
+
+ def initialize(name)
+ @name = name
+
+ @confines = []
+ @chunks = {}
+
+ @aggregate = nil
+ @deps = Facter::Core::DirectedGraph.new
+ end
+
+ def set_options(options)
+ if options[:name]
+ @name = options.delete(:name)
+ end
+
+ if options.has_key?(:timeout)
+ @timeout = options.delete(:timeout)
+ end
+
+ if options.has_key?(:weight)
+ @weight = options.delete(:weight)
+ end
+
+ if not options.keys.empty?
+ raise ArgumentError, "Invalid aggregate options #{options.keys.inspect}"
+ end
+ end
+
+ # Define a new chunk for the given aggregate
+ #
+ # @api public
+ #
+ # @example Defining a chunk with no dependencies
+ # aggregate.chunk(:mountpoints) do
+ # # generate mountpoint information
+ # end
+ #
+ # @example Defining an chunk to add mount options
+ # aggregate.chunk(:mount_options, :require => [:mountpoints]) do |mountpoints|
+ # # `mountpoints` is the result of the previous chunk
+ # # generate mount option information based on the mountpoints
+ # end
+ #
+ # @param name [Symbol] A name unique to this aggregate describing the chunk
+ # @param opts [Hash]
+ # @options opts [Array<Symbol>, Symbol] :require One or more chunks
+ # to evaluate and pass to this block.
+ # @yield [*Object] Zero or more chunk results
+ #
+ # @return [void]
+ def chunk(name, opts = {}, &block)
+ if not block_given?
+ raise ArgumentError, "#{self.class.name}#chunk requires a block"
+ end
+
+ deps = Array(opts.delete(:require))
+
+ if not opts.empty?
+ raise ArgumentError, "Unexpected options passed to #{self.class.name}#chunk: #{opts.keys.inspect}"
+ end
+
+ @deps[name] = deps
+ @chunks[name] = block
+ end
+
+ # Define how all chunks should be combined
+ #
+ # @api public
+ #
+ # @example Merge all chunks
+ # aggregate.aggregate do |chunks|
+ # final_result = {}
+ # chunks.each_value do |chunk|
+ # final_result.deep_merge(chunk)
+ # end
+ # final_result
+ # end
+ #
+ # @example Sum all chunks
+ # aggregate.aggregate do |chunks|
+ # total = 0
+ # chunks.each_value do |chunk|
+ # total += chunk
+ # end
+ # total
+ # end
+ #
+ # @yield [Hash<Symbol, Object>] A hash containing chunk names and
+ # chunk values
+ #
+ # @return [void]
+ def aggregate(&block)
+ if block_given?
+ @aggregate = block
+ else
+ raise ArgumentError, "#{self.class.name}#aggregate requires a block"
+ end
+ end
+
+ private
+
+ # Evaluate the results of this aggregate.
+ #
+ # @see Facter::Core::Resolvable#value
+ # @return [Object]
+ def resolve_value
+ chunk_results = run_chunks()
+ aggregate_results(chunk_results)
+ end
+
+ # Order all chunks based on their dependencies and evaluate each one, passing
+ # dependent chunks as needed.
+ #
+ # @return [Hash<Symbol, Object>] A hash containing the chunk that
+ # generated value and the related value.
+ def run_chunks
+ results = {}
+ order_chunks.each do |(name, block)|
+ input = @deps[name].map { |dep_name| results[dep_name] }
+
+ results[name] = block.call(*input)
+ end
+
+ results
+ end
+
+ # Process the results of all chunks with the aggregate block and return the results.
+ # @return [Object]
+ def aggregate_results(results)
+ @aggregate.call(results)
+ end
+
+ # Order chunks based on their dependencies
+ #
+ # @return [Array<Symbol, Proc>] A list of chunk names and blocks in evaluation order.
+ def order_chunks
+ if not @deps.acyclic?
+ raise DependencyError, "Could not order chunks; found the following dependency cycles: #{@deps.cycles.inspect}"
+ end
+
+ sorted_names = @deps.tsort
+
+ sorted_names.map do |name|
+ [name, @chunks[name]]
+ end
+ end
+
+ class DependencyError < StandardError; end
+end
diff --git a/lib/facter/core/resolvable.rb b/lib/facter/core/resolvable.rb
index 420e0ab..7228262 100644
--- a/lib/facter/core/resolvable.rb
+++ b/lib/facter/core/resolvable.rb
@@ -65,7 +65,6 @@ module Facter::Core::Resolvable
end
Facter::Util::Normalization.normalize(result)
-
rescue Timeout::Error => detail
Facter.warn "Timed out seeking value for #{self.name}"
diff --git a/spec/unit/core/aggregate_spec.rb b/spec/unit/core/aggregate_spec.rb
new file mode 100644
index 0000000..e1d9ab4
--- /dev/null
+++ b/spec/unit/core/aggregate_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+require 'facter/core/aggregate'
+
+describe Facter::Core::Aggregate do
+
+ subject do
+ obj = described_class.new('aggregated')
+ obj.aggregate { |chunks| chunks.values }
+ obj
+ end
+
+ it "can be resolved" do
+ expect(subject).to be_a_kind_of Facter::Core::Resolvable
+ end
+
+ it "can be confined and weighted" do
+ expect(subject).to be_a_kind_of Facter::Core::Suitable
+ end
+
+ describe "setting options" do
+
+ it "can set the timeout" do
+ subject.set_options(:timeout => 314)
+ expect(subject.limit).to eq 314
+ end
+
+ it "can set the weight" do
+ subject.set_options(:weight => 27)
+ expect(subject.weight).to eq 27
+ end
+
+ it "can set the name" do
+ subject.set_options(:name => 'something')
+ expect(subject.name).to eq 'something'
+ end
+
+ it "fails on unhandled options" do
+ expect do
+ subject.set_options(:foo => 'bar')
+ end.to raise_error(ArgumentError, /Invalid aggregate options .*foo/)
+ end
+ end
+
+ describe "declaring chunks" do
+ it "requires that an chunk is given a block" do
+ expect { subject.chunk(:fail) }.to raise_error(ArgumentError, /requires a block/)
+ end
+
+ it "allows an chunk to have a list of requirements" do
+ subject.chunk(:data, :require => [:other]) { }
+ expect(subject.deps[:data]).to eq [:other]
+ end
+
+ it "converts a single chunk requirement to an array" do
+ subject.chunk(:data, :require => :other) { }
+ expect(subject.deps[:data]).to eq [:other]
+ end
+
+ it "raises an error when an unhandled option is passed" do
+ expect {
+ subject.chunk(:data, :before => [:other]) { }
+ }.to raise_error(ArgumentError, /Unexpected options.*#chunk: .*before/)
+ end
+ end
+
+ describe "handling interactions between chunks" do
+ it "generates a warning when there is a dependency cycle in chunks" do
+ subject.chunk(:first, :require => [:second]) { }
+ subject.chunk(:second, :require => [:first]) { }
+
+ Facter.expects(:warn) do |msg|
+ expect(msg).to match /dependency cycles: .*[:first, :second]/
+ end
+
+ subject.value
+ end
+
+ it "passes all requested chunk results to the depending chunk" do
+ subject.chunk(:first) { 'foo' }
+ subject.chunk(:second, :require => [:first]) do |first|
+ "#{first} bar"
+ end
+
+ output = subject.value
+ expect(output).to include 'foo'
+ expect(output).to include 'foo bar'
+ end
+
+ it "clones and freezes chunk results passed to other chunks"
+ end
+
+ describe "evaluating chunks" do
+ it "emits a warning and returns nil when a chunk raises an error" do
+ Facter.expects(:warn) do |msg|
+ expect(msg).to match /Could not run chunk boom.*kaboom!/
+ end
+
+ subject.chunk(:boom) { raise 'kaboom!' }
+ subject.value
+ end
+ end
+
+ describe "aggregating chunks" do
+ it "passes all chunk results as a hash to the aggregate block" do
+ subject.chunk(:data) { 'data chunk' }
+ subject.chunk(:datum) { 'datum chunk' }
+
+ subject.aggregate do |chunks|
+ expect(chunks).to eq(:data => 'data chunk', :datum => 'datum chunk')
+ end
+
+ subject.value
+ end
+
+ it "uses the result of the aggregate block as the value" do
+ subject.aggregate { "who needs chunks anyways" }
+ expect(subject.value).to eq "who needs chunks anyways"
+ end
+
+ it "generates a warning and returns if the aggregate raises an error" do
+ subject.aggregate { raise 'kaboom!' }
+
+ Facter.expects(:warn) do |msg|
+ expect(msg).to match /Could not aggregate chunks for boom.*kaboom!/
+ end
+
+ subject.value
+ end
+ end
+end
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-puppet/facter.git
More information about the Pkg-puppet-devel
mailing list