module Halite::SpecHelper::ClassMethods

@!classmethods

Public Instance Methods

action_run() click to toggle source

Blank action because I do that so much.

# File lib/halite/spec_helper.rb, line 391
def action_run
end
halite_helpers() click to toggle source

Storage for helper-defined resources and providers to find them for parent lookups if needed.

@api private @return [Hash<Symbol, Hash<Symbol, Class>>]

# File lib/halite/spec_helper.rb, line 444
def halite_helpers
  @halite_helpers ||= {resources: {}, providers: {}}
end
included(klass) click to toggle source
Calls superclass method
# File lib/halite/spec_helper.rb, line 434
def included(klass)
  super
  klass.extend ClassMethods
end
load_current_resource() click to toggle source

Default blank impl to avoid error.

# File lib/halite/spec_helper.rb, line 387
def load_current_resource
end
provider(name, auto: true, rspec: true, parent: Chef::Provider, patch: true, defined_at: caller[0], &block) click to toggle source

Define a provider class for use in an example group. By default a :run action will be created, load_current_resource will be defined as a no-op, and the RSpec matchers will be available inside the provider.

@param name [Symbol] Name for the provider in snake-case. @param options [Hash] Provider options. @option options [Class, Symbol] :parent (Chef::Provider) Parent class

for the provider. If a symbol is given, it corresponds to another
resource defined via this helper.

@option options [Boolean] :auto (true) Create action_run and

load_current_resource.

@option options [Boolean] :rspec (true) Include RSpec matchers in the

provider.

@param block [Proc] Body of the provider class. Optional. @example

describe MyMixin do
  resource(:my_resource)
  provider(:my_resource) do
    include Poise
    def action_run
      ruby_block 'test'
    end
  end
  recipe do
    my_resource 'test'
  end
  it { is_expected.to run_my_resource('test') }
  it { is_expected.to run_ruby_block('test') }
end
# File lib/halite/spec_helper.rb, line 372
def provider(name, auto: true, rspec: true, parent: Chef::Provider, patch: true, defined_at: caller[0], &block)
  parent = providers[parent] if parent.is_a?(Symbol)
  raise Halite::Error.new("Parent class for #{name} is not a class: #{parent.inspect}") unless parent.is_a?(Class)
  # Pull out the example group for use in the class.
  example_group = self
  # Create the provider class.
  provider_class = Class.new(parent) do
    # Pull in RSpec expectations.
    if rspec
      include RSpec::Matchers
      include RSpec::Mocks::ExampleMethods
    end

    if auto
      # Default blank impl to avoid error.
      def load_current_resource
      end

      # Blank action because I do that so much.
      def action_run
      end
    end

    # Make the anonymous class pretend to have a name.
    define_singleton_method(:name) do
      'Chef::Provider::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s)
    end

    # Helper for debugging, shows where the class was defined.
    define_singleton_method(:halite_defined_at) do
      defined_at
    end

    # Create magic delegators for various metadata.
    {
      example_group: example_group,
      described_class: example_group.metadata[:described_class],
    }.each do |key, value|
      define_method(key) { value }
      define_singleton_method(key) { value }
    end

    # Evaluate the class body.
    class_exec(&block) if block
  end

  # Clean up any global registration that happens on class compile.
  Patcher.post_create_cleanup(name, provider_class) if patch

  # Store for use up with the parent system
  halite_helpers[:providers][name.to_sym] = provider_class

  around do |ex|
    if patch && provider(name) == provider_class
      # We haven't been overridden from a nested scope.
      Patcher.patch(name, provider_class) { ex.run }
    else
      ex.run
    end
  end
end
providers() click to toggle source

Find all helper-defined providers in the current context and parents.

@api private @return [Hash<Symbol, Class>]

# File lib/halite/spec_helper.rb, line 466
def providers
  ([self] + parent_groups).reverse.inject({}) do |memo, group|
    begin
      memo.merge(group.halite_helpers[:providers] || {})
    rescue NoMethodError
      memo
    end
  end
end
recipe(*recipe_names, subject: true, &block) click to toggle source

Define a recipe to be run via ChefSpec and used as the subject of this example group. You can specify either a single recipe block or one-or-more recipe names.

@param recipe_names [Array<String>] Recipe names to converge for this test. @param block [Proc] Recipe to converge for this test. @param subject [Boolean] If true, this recipe should be the subject of

this test.

@example Using a recipe block

describe 'my recipe' do
  recipe do
    ruby_block 'test'
  end
  it { is_expected.to run_ruby_block('test') }
end

@example Using external recipes

describe 'my recipe' do
  recipe 'my_recipe'
  it { is_expected.to run_ruby_block('test') }
end
# File lib/halite/spec_helper.rb, line 182
def recipe(*recipe_names, subject: true, &block)
  # Keep the actual logic in a let in case I want to define the subject as something else
  let(:chef_run) { recipe_names.empty? ? chef_runner.converge_block(&block) : chef_runner.converge(*recipe_names) }
  subject { chef_run } if subject
end
resource(name, auto: true, parent: Chef::Resource, step_into: true, unwrap_notifying_block: true, patch: true, defined_at: caller[0], &block) click to toggle source

Define a resource class for use in an example group. By default the :run action will be set as the default.

@param name [Symbol] Name for the resource in snake-case. @param options [Hash] Resource options. @option options [Class, Symbol] :parent (Chef::Resource) Parent class

for the resource. If a symbol is given, it corresponds to another
resource defined via this helper.

@option options [Boolean] :auto (true) Set the resource name correctly

and use :run as the default action.

@param block [Proc] Body of the resource class. Optional. @example

describe MyMixin do
  resource(:my_resource) do
    include Poise
    attribute(:path, kind_of: String)
  end
  provider(:my_resource)
  recipe do
    my_resource 'test' do
      path '/tmp'
    end
  end
  it { is_expected.to run_my_resource('test').with(path: '/tmp') }
end
# File lib/halite/spec_helper.rb, line 269
def resource(name, auto: true, parent: Chef::Resource, step_into: true, unwrap_notifying_block: true, patch: true, defined_at: caller[0], &block)
  parent = resources[parent] if parent.is_a?(Symbol)
  raise Halite::Error.new("Parent class for #{name} is not a class: #{parent.inspect}") unless parent.is_a?(Class)
  # Pull out the example group for use in the class.
  example_group = self
  # Create the resource class.
  resource_class = Class.new(parent) do
    # Make the anonymous class pretend to have a name.
    define_singleton_method(:name) do
      'Chef::Resource::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s)
    end

    # Helper for debugging, shows where the class was defined.
    define_singleton_method(:halite_defined_at) do
      defined_at
    end

    # Create magic delegators for various metadata.
    {
      example_group: example_group,
      described_class: example_group.metadata[:described_class],
    }.each do |key, value|
      define_method(key) { value }
      define_singleton_method(key) { value }
    end

    # Evaluate the class body.
    class_exec(&block) if block

    # Optional initialization steps. Disable for special unicorn tests.
    if auto
      # Fill in a :run action by default.
      old_init = instance_method(:initialize)
      define_method(:initialize) do |*args|
        old_init.bind(self).call(*args)
        # Fill in the resource name because I know it, but don't
        # overwrite because a parent might have done this already.
        @resource_name = name.to_sym
        # ChefSpec doesn't seem to work well with action :nothing
        if Array(@action) == [:nothing]
          @action = :run
          @allowed_actions |= [:run]
        end
        if defined?(self.class.default_action) && Array(self.class.default_action) == [:nothing]
          self.class.default_action(:run)
        end
      end
    end
  end

  # Try to set the resource name for 12.4+.
  if defined?(resource_class.resource_name)
    resource_class.resource_name(name)
  end

  # Clean up any global registration that happens on class compile.
  Patcher.post_create_cleanup(name, resource_class) if patch

  # Store for use up with the parent system
  halite_helpers[:resources][name.to_sym] = resource_class

  # Automatically step in to our new resource
  step_into(resource_class, name, unwrap_notifying_block: unwrap_notifying_block) if step_into

  around do |ex|
    if patch && resource(name) == resource_class
      # We haven't been overridden from a nested scope.
      Patcher.patch(name, resource_class) { ex.run }
    else
      ex.run
    end
  end
end
resources() click to toggle source

Find all helper-defined resources in the current context and parents.

@api private @return [Hash<Symbol, Class>]

# File lib/halite/spec_helper.rb, line 452
def resources
  ([self] + parent_groups).reverse.inject({}) do |memo, group|
    begin
      memo.merge(group.halite_helpers[:resources] || {})
    rescue NoMethodError
      memo
    end
  end
end
step_into(name=nil, resource_name=nil, unwrap_notifying_block: true) click to toggle source

Configure ChefSpec to step in to a resource/provider. This will also automatically create ChefSpec matchers for the resource.

@overload step_into(name)

@param name [String, Symbol] Name of the resource in snake-case.

@overload step_into(resource, resource_name)

@param resource [Class] Resource class to step in to.
@param resource_name [String, Symbol, nil] Name of the given resource in snake-case.

@example

describe 'my_lwrp' do
  step_into(:my_lwrp)
  recipe do
    my_lwrp 'test'
  end
  it { is_expected.to run_ruby_block('test') }
end
Calls superclass method
# File lib/halite/spec_helper.rb, line 204
def step_into(name=nil, resource_name=nil, unwrap_notifying_block: true)
  return super() if name.nil?
  resource_class = if name.is_a?(Class)
    name
  elsif resources[name.to_sym]
    # Handle cases where the resource has defined via a helper with
    # step_into:false but a nested example wants to step in.
    resources[name.to_sym]
  else
    # Won't see platform/os specific resources but not sure how to fix
    # that. I need the class here for the matcher creation below.
    Chef::Resource.resource_for_node(name.to_sym, Chef::Node.new)
  end
  resource_name ||= if resource_class.respond_to?(:resource_name)
    resource_class.resource_name
  else
    Chef::Mixin::ConvertToClassName.convert_to_snake_case(resource_class.name.split('::').last)
  end

  # Patch notifying_block from Poise::Provider to just run directly.
  # This is not a great solution but it is better than nothing for right
  # now. In the future this should maybe do an internal converge but using
  # ChefSpec somehow?
  if unwrap_notifying_block
    old_provider_for_action = resource_class.instance_method(:provider_for_action)
    resource_class.send(:define_method, :provider_for_action) do |*args|
      old_provider_for_action.bind(self).call(*args).tap do |provider|
        if provider.respond_to?(:notifying_block, true)
          provider.define_singleton_method(:notifying_block) do |&block|
            block.call
          end
        end
      end
    end
  end

  # Add to the let variable passed in to ChefSpec.
  super(resource_name)
end