[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